GithubHelp home page GithubHelp logo

azman-v2's Introduction

azman-v2's People

Contributors

jpda avatar

Stargazers

 avatar  avatar  avatar

Watchers

 avatar  avatar  avatar

Forkers

cocallaw

azman-v2's Issues

move this as default to configuration

move this as default to configuration

// todo: move this as default to configuration

        {
            // connect to azure
            var resourceManagerClient = new ResourcesManagementClient(request.SubscriptionId, _tokenCredential);

            try
            {
                var resourceGroupRequest = await resourceManagerClient.ResourceGroups.GetAsync(request.ResourceGroupName);

                if (resourceGroupRequest == null) return;
                var resourceGroup = resourceGroupRequest.Value;

                // todo: move this as default to configuration
                var newDate = request.DateCreated.AddDays(30).Date;

                resourceGroup.Tags.TryAdd("expires", newDate.ToString("yyyy-MM-dd"));
                resourceGroup.Tags.TryAdd("tagged-by", "thrazman");
                resourceGroup.Tags.TryAdd("owner", request.CreatedByUser);

                // service-msft-prod-azman-main-compute
                var groupNamePieces = resourceGroup.Name.Split('-');
                if (groupNamePieces.Count() >= 3)
                {
                    resourceGroup.Tags.TryAdd("function", groupNamePieces[0]); // service
                    resourceGroup.Tags.TryAdd("customer", groupNamePieces[1]); // msft
                    resourceGroup.Tags.TryAdd("env", groupNamePieces[2]); // prod
                    resourceGroup.Tags.TryAdd("project", string.Join('-', groupNamePieces.Skip(3))); //azman-main-compute
                }
                await resourceManagerClient.ResourceGroups.CreateOrUpdateAsync(resourceGroup.Name, resourceGroup);
            }
            catch (System.Exception ex)
            {
                _log.LogError(ex, ex.Message);
                throw;
            }
        }

        // todo: explore changes required for using resource ID for _any_ resource

7ab7f422c2a2f536a1b004f2f3d3b3e03b3f06f3

what happens when tagging fails? what's recoverabl...

what happens when tagging fails? what's recoverable? what's not?

// todo: what happens when tagging fails? what's recoverable? what's not?


            // handles queueing up new resource groups to be tagged
            return new TagSuiteModel(
                groupName: alert.data.context.activityLog.resourceGroupName,
                subscriptionId: alert.data.context.activityLog.subscriptionId,
                user: alert.data.context.activityLog.caller,
                managementDate: alert.data.context.activityLog.eventTimestamp
            );
        }

        public async Task AddTagSuite(TagSuiteModel request)
        {
            request.GenerateBaseTags();
            await AddTags(request);
        }

        public async Task AddTags(TagModel request)
        {
            var resourceManagerClient = new ResourcesManagementClient(request.SubscriptionId, _tokenCredential);
            var resourceGroupRequest = await resourceManagerClient.ResourceGroups.GetAsync(request.ResourceGroupName);
            if (resourceGroupRequest == null) return;
            var resourceGroup = resourceGroupRequest.Value;
            try
            {
                foreach (var t in request.Tags)
                {
                    resourceGroup.Tags.TryAdd(t.Key, t.Value);
                }
                await resourceManagerClient.ResourceGroups.CreateOrUpdateAsync(resourceGroup.Name, resourceGroup);
            }
            catch (Exception ex)
            {
                // todo: what happens when tagging fails? what's recoverable? what's not?
                _log.LogError(ex, ex.Message);
            }
        }

        public async Task AddTags(string resourceGroup, string subscriptionId, params KeyValuePair<string, string>[] tags)
        {
            await AddTags(new TagModel(resourceGroup, subscriptionId, tags));
        }

        // todo: explore changes required for using resource ID for _any_ resource
        public async Task DeleteResource(string subscriptionId, string resourceGroupName)
        {

e765261e4f672928e5bd499f85931420db9c12d1

update the return types with more arbitrary data

update the return types with more arbitrary data

// todo: update the return types with more arbitrary data

            return await QueryResourceGraph(expiredQuery);
        }

        public async Task<IEnumerable<ResourceSearchResult>> FindSpecificResources(string expression)
        { // https://docs.microsoft.com/en-us/azure/virtual-machines/states-lifecycle
            var runningVmsQuery = $@"resources | where type == 'microsoft.compute/virtualmachines'
                                    | project id, subscriptionId, resourceGroup, name, 
                                    properties.extended.instanceView.powerState.displayStatus";
            // todo: update the return types with more arbitrary data
            return await QueryResourceGraph(runningVmsQuery);
        }

        // public async Task<IEnumerable<T>> FindSpecificResources<T>(string expression, Func<)
        // {
            
        // }
    }
}
 No newline at end of file

52e3802884b4cde0bf5d4f4d950563583ca15d8e

parse the token for expiration? dunno

parse the token for expiration? dunno

// todo: parse the token for expiration? dunno


        public AccessTokenResponse GetAccessToken(string[] scopes, bool forceRefresh = false)
        {
            // todo: parse the token for expiration? dunno
            var resource = ScopeUtil.GetResourceFromScope(scopes);
            _log.LogTrace($"Fetching access token via MSI for {resource} ({string.Join(',', scopes)}); forcedRefresh: {forceRefresh}");
            return new AccessTokenResponse(resource, _tokenProvider.GetAccessTokenAsync(resource, forceRefresh).Result, DateTimeOffset.UtcNow.AddHours(1));
        }

        public async Task<AccessTokenResponse> GetAccessTokenAsync(string[] scopes, bool forceRefresh = false)
        {
            // todo: parse the token for expiration? dunno
            var resource = ScopeUtil.GetResourceFromScope(scopes);
            _log.LogTrace($"Fetching access token via MSI for {resource} ({string.Join(',', scopes)}); forcedRefresh: {forceRefresh}");
            return new AccessTokenResponse(resource, await _tokenProvider.GetAccessTokenAsync(resource, forceRefresh), DateTimeOffset.UtcNow.AddHours(1));
        }
    }
}
 No newline at end of file
ndex 1994dc7..80ffd98 100644
++ b/AzureResourceManagementService.cs

35fd39a9636fc96532248bcc170adf162360bd14

make sure i'm not missing something here

make sure i'm not missing something here

// todo: make sure i'm not missing something here

            var resource = ScopeUtil.GetResourceFromScope(scopes);
            _log.LogTrace($"Fetching access token via CLI for {resource} ({string.Join(',', scopes)}); forcedRefresh: {forceRefresh}");
            // todo: make sure i'm not missing something here
            if (!_tokens.ContainsKey(resource) || forceRefresh)
            {
                _tokens.AddOrUpdate(resource, x => GetToken(resource), (y, z) => GetToken(resource));
            }
            return _tokens.GetOrAdd(resource, x => GetToken(resource));
        }

        public Task<AccessTokenResponse> GetAccessTokenAsync(string[] scopes, bool forceRefresh = false)
ndex 80ffd98..49f2528 100644
++ b/AzureResourceManagementService.cs

5ba38e3b71179f5fce02b8f46c4dd5aef3b9688d

tweak based on output and ease of re-deploy

tweak based on output and ease of re-deploy

// todo: tweak based on output and ease of re-deploy

using System.Threading.Tasks;
using Microsoft.Extensions.Logging;

using Azure.Identity;
using Azure.ResourceManager.Resources;

using Microsoft.Azure.Services.AppAuthentication;
using azman_v2.Model;

namespace azman_v2
{
    public class AzureResourceManagementService : IResourceManagementService
    {
        private readonly HttpClient _httpClient;
        private readonly ILogger<AzureResourceManagementService> _log;
        public AzureResourceManagementService(IHttpClientFactory httpFactory, ILoggerFactory loggerFactory)
        {
            _httpClient = httpFactory.CreateClient();
            _log = loggerFactory.CreateLogger<AzureResourceManagementService>();
        }

        public TaggingRequestModel? ProcessAlert(dynamic alert)
        {
            if (!string.Equals(alert.data.context.activityLog.status.ToString(), "Succeeded", StringComparison.OrdinalIgnoreCase) ||
                !string.Equals(alert.data.context.activityLog.subStatus.ToString(), "Created", StringComparison.OrdinalIgnoreCase))
            {
                _log.LogTrace($"status: {alert.data.context.activityLog.status}");
                _log.LogTrace($"subStatus: {alert.data.context.activityLog.subStatus}");
                //return new OkResult(); // return 200, we're done here since it hasn't succeeded yet
                return null;
            }

            _log.LogTrace($"{alert.data.context.activityLog.resourceGroupName}");

            // handles queueing up new resource groups to be tagged
            return new TaggingRequestModel(
                groupName: alert.data.context.activityLog.resourceGroupName,
                user: alert.data.context.activityLog.caller,
                subscriptionId: alert.data.context.activityLog.subscriptionId,
                created: alert.data.context.activityLog.eventTimestamp
            );
        }

        public async Task TagResource(TaggingRequestModel request)
        {
            // connect to azure
            var resourceManagerClient = new ResourcesManagementClient(request.SubscriptionId, new DefaultAzureCredential());
            var resourceGroupRequest = await resourceManagerClient.ResourceGroups.GetAsync(request.ResourceGroupName);

            if (resourceGroupRequest == null) return;
            var resourceGroup = resourceGroupRequest.Value;

            // todo: move this as default to configuration
            var newDate = request.DateCreated.AddDays(30).Date;

            resourceGroup.Tags.TryAdd("expires", newDate.ToString("yyyy-MM-dd"));
            resourceGroup.Tags.TryAdd("tagged-by", "thrazman");
            resourceGroup.Tags.TryAdd("owner", request.CreatedByUser);

            // service-msft-prod-azman-main-compute
            var groupNamePieces = resourceGroup.Name.Split('-');
            if (groupNamePieces.Count() >= 3)
            {
                resourceGroup.Tags.TryAdd("function", groupNamePieces[0]); // service
                resourceGroup.Tags.TryAdd("customer", groupNamePieces[1]); // msft
                resourceGroup.Tags.TryAdd("env", groupNamePieces[2]); // prod
                resourceGroup.Tags.TryAdd("project", string.Join('-', groupNamePieces.Skip(3))); //azman-main-compute
            }
            await resourceManagerClient.ResourceGroups.CreateOrUpdateAsync(resourceGroup.Name, resourceGroup);
        }

        // todo: explore changes required for using resource ID for _any_ resource
        public async Task DeleteResource(string subscriptionId, string resourceGroupName)
        {
            // todo: what-if? --> log deletion, but don't execute
            // connect to azure
            _log.LogInformation($"Request to delete {resourceGroupName} from subscription {subscriptionId}");
            var resourceManagerClient = new ResourcesManagementClient(subscriptionId, new DefaultAzureCredential());
            await resourceManagerClient.ResourceGroups.StartDeleteAsync(resourceGroupName);
        }

        public async Task<string> ExportResourceGroupTemplateByName(string subscriptionId, string groupName)
        {
            // todo: tweak based on output and ease of re-deploy
            var resourceManagerClient = new ResourcesManagementClient(subscriptionId, new DefaultAzureCredential());
            var exportedTemplate = await resourceManagerClient.ResourceGroups.StartExportTemplateAsync(groupName,
                new Azure.ResourceManager.Resources.Models.ExportTemplateRequest());
            if (exportedTemplate.HasValue) return (string)exportedTemplate.Value.Template; // todo: y tho?
            return string.Empty;
        }
    }
}
 No newline at end of file
ew file mode 100644
ndex 0000000..2336ca1
++ b/IResourceManagementService.cs

45e33f7590fba875e15d2a775d0bbcc7a1a0207d

this is required but read-only? hmm

this is required but read-only? hmm

// todo: this is required but read-only? hmm

        {
            var token = await _tokenProvider.GetAccessTokenAsync(new[] { "https://management.azure.com/" });
            _httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token.Token);
            var body = new StringContent("{'resources':[ '*' ]}", System.Text.Encoding.UTF8, "application/json");

            var request = await _httpClient.PostAsync($"https://management.azure.com/subscriptions/{subscriptionId}/resourcegroups/{groupName}/exportTemplate?api-version=2020-06-01", body);
            if (!request.IsSuccessStatusCode) return string.Empty;

            // todo: handle 202 accepted on export request, sigh

            var templateData = await request.Content.ReadAsStringAsync();
            return templateData;


            // POST https://management.azure.com/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/exportTemplate?api-version=2020-06-01
            // todo: tweak based on output and ease of re-deploy
            // var resourceManagerClient = new ResourcesManagementClient(subscriptionId, _tokenCredential);
            // var exportedTemplate = await resourceManagerClient.ResourceGroups.StartExportTemplateAsync(groupName,
            // todo: this is required but read-only? hmm
            //new Azure.ResourceManager.Resources.Models.ExportTemplateRequest() { Resources = new[] { "*" } });
            // new Azure.ResourceManager.Resources.Models.ExportTemplateRequest() { });
            // if (exportedTemplate.HasValue) return (string)exportedTemplate.Value.Template; // todo: y tho?
            //return string.Empty;
        }
    }
}
 No newline at end of file
ndex 85f3af9..3c4161a 100644
++ b/Functions.cs

867b86203e22dc1451c38468fbc8ab30d421d07c

y tho?

y tho?

if (exportedTemplate.HasValue) return (string)exportedTemplate.Value.Template; // todo: y tho?

        {
            // todo: tweak based on output and ease of re-deploy
            var resourceManagerClient = new ResourcesManagementClient(subscriptionId, new DefaultAzureCredential());
            var exportedTemplate = await resourceManagerClient.ResourceGroups.StartExportTemplateAsync(groupName,
                new Azure.ResourceManager.Resources.Models.ExportTemplateRequest());
            if (exportedTemplate.HasValue) return (string)exportedTemplate.Value.Template; // todo: y tho?
            return string.Empty;
        }
    }

7f6b34a8a4f8b75c46de4daa7365b591ba0f9fdb

best candidate for durable functions

best candidate for durable functions

// todo: best candidate for durable functions

        }

        // todo: best candidate for durable functions
        [FunctionName("ResourceGroupExpired")]
        public async Task ResourceGroupExpired(
            [QueueTrigger("%ResourceGroupExpiredQueueName%", Connection = "MainStorageConnection")] ResourceSearchResult request,
ndex 9937d22..5cb32e3 100644
++ b/OnResourceGroupCreate.cs

f37009bb7a6dadc2592c058549f29303e5a75bf0

parse the token for expiration? dunno

parse the token for expiration? dunno

// todo: parse the token for expiration? dunno

        }

        public AccessTokenResponse GetAccessToken(string[] scopes, bool forceRefresh = false)
        {
            // todo: parse the token for expiration? dunno
            var resource = ScopeUtil.GetResourceFromScope(scopes);
            _log.LogTrace($"Fetching access token via MSI for {resource} ({string.Join(',', scopes)}); forcedRefresh: {forceRefresh}");
            return new AccessTokenResponse(resource, _tokenProvider.GetAccessTokenAsync(resource, forceRefresh).Result, DateTimeOffset.UtcNow.AddHours(1));
        }

        public async Task<AccessTokenResponse> GetAccessTokenAsync(string[] scopes, bool forceRefresh = false)
        {
            // todo: parse the token for expiration? dunno
            var resource = ScopeUtil.GetResourceFromScope(scopes);
            _log.LogTrace($"Fetching access token via MSI for {resource} ({string.Join(',', scopes)}); forcedRefresh: {forceRefresh}");
            return new AccessTokenResponse(resource, await _tokenProvider.GetAccessTokenAsync(resource, forceRefresh), DateTimeOffset.UtcNow.AddHours(1));
        }
    }
}
 No newline at end of file
ndex 1994dc7..80ffd98 100644
++ b/AzureResourceManagementService.cs

418ce66244aac9661212f6f23aa3f1ff31d792c0

y tho?

y tho?

// if (exportedTemplate.HasValue) return (string)exportedTemplate.Value.Template; // todo: y tho?

            var token = await _tokenProvider.GetAccessTokenAsync(new[] { "https://management.azure.com/" });
            _httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token.Token);
            var body = new StringContent("{'resources':[ '*' ]}", System.Text.Encoding.UTF8, "application/json");

            var request = await _httpClient.PostAsync($"https://management.azure.com/subscriptions/{subscriptionId}/resourcegroups/{groupName}/exportTemplate?api-version=2020-06-01", body);
            if (!request.IsSuccessStatusCode) return string.Empty;

            // todo: handle 202 accepted on export request, sigh

            var templateData = await request.Content.ReadAsStringAsync();
            return templateData;


            // POST https://management.azure.com/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/exportTemplate?api-version=2020-06-01
            // todo: tweak based on output and ease of re-deploy
            // var resourceManagerClient = new ResourcesManagementClient(subscriptionId, _tokenCredential);
            // var exportedTemplate = await resourceManagerClient.ResourceGroups.StartExportTemplateAsync(groupName,
            // todo: this is required but read-only? hmm
            //new Azure.ResourceManager.Resources.Models.ExportTemplateRequest() { Resources = new[] { "*" } });
            // new Azure.ResourceManager.Resources.Models.ExportTemplateRequest() { });
            // if (exportedTemplate.HasValue) return (string)exportedTemplate.Value.Template; // todo: y tho?
            //return string.Empty;
        }
    }
}
 No newline at end of file
ndex 85f3af9..3c4161a 100644
++ b/Functions.cs

b4598f74e595b171d5c2e576e325b98db13e2194

hack for testing until configurable subscription l...

hack for testing until configurable subscription list is available

// todo: hack for testing until configurable subscription list is available

        private async Task<IEnumerable<string>> FindAccessibleSubscriptions(bool forceRefresh = false)
        {
            // todo: hack for testing until configurable subscription list is available
            _subscriptionIds.Add("e7048bdb-835c-440f-9304-aa4171382839");
            if (!forceRefresh || _subscriptionIds.Any())
            {

05da050c71063ea0326410206d81eb94db95a2a1

tweak based on output and ease of re-deploy

tweak based on output and ease of re-deploy

// todo: tweak based on output and ease of re-deploy

        public async Task<string> ExportResourceGroupTemplateByName(string subscriptionId, string groupName)
        {
            // todo: tweak based on output and ease of re-deploy
            var resourceManagerClient = new ResourcesManagementClient(subscriptionId, new DefaultAzureCredential());
            var exportedTemplate = await resourceManagerClient.ResourceGroups.StartExportTemplateAsync(groupName,
                new Azure.ResourceManager.Resources.Models.ExportTemplateRequest());
            if (exportedTemplate.HasValue) return (string)exportedTemplate.Value.Template; // todo: y tho?
            return string.Empty;
        }
    }

b1929f3de25a03222d4a4c486eaf2bff5f3f26b2

configuration for national clouds, e.g., https://m...

configuration for national clouds, e.g., https://management.chinacloudapi.cn

// todo: configuration for national clouds, e.g., https://management.chinacloudapi.cn

using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Azure.Management.ResourceGraph;
using Microsoft.Azure.Management.ResourceGraph.Models;

namespace azman_v2
{
    public class Scanner : IScanner
    {
        private readonly ITokenProvider _tokenProvider;
        private readonly HttpClient _httpClient;
        private readonly ILogger<Scanner> _log;

        // todo: configuration for national clouds, e.g., https://management.chinacloudapi.cn
        private readonly string _managementEndpoint = "https://management.azure.com/";
        private readonly string _managementAzureAdResourceId = "https://management.azure.com/";
        // todo: configuration to allow/deny specific subscriptions
        private readonly List<string> _subscriptionIds;

        public Scanner(ITokenProvider tokenProvider, IHttpClientFactory httpFactory, ILoggerFactory loggerFactory)
        {
            _tokenProvider = tokenProvider;
            _httpClient = httpFactory.CreateClient();
            _log = loggerFactory.CreateLogger<Scanner>();
            _subscriptionIds = new List<string>();
        }

        private async Task<IEnumerable<string>> FindAccessibleSubscriptions(bool forceRefresh = false)
        {
            // todo: hack for testing until configurable subscription list is available
            _subscriptionIds.Add("e7048bdb-835c-440f-9304-aa4171382839");
            if (!forceRefresh || _subscriptionIds.Any())
            {
                return _subscriptionIds;
            }

            _log.LogTrace($"Getting subscription list starting at ${DateTime.UtcNow}");
            _log.LogTrace($"Getting access token for ${_managementAzureAdResourceId}");

            var token = await _tokenProvider.GetAccessTokenAsync(_managementAzureAdResourceId, false);
            _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);

            // resource graph is expecting an array of subscriptions, so get the subscription list first
            var subRequest = await _httpClient.GetAsync($"{_managementEndpoint}subscriptions?api-version=2020-01-01");
            if (!subRequest.IsSuccessStatusCode)
            {
                _log.LogError(new EventId((int)subRequest.StatusCode, subRequest.StatusCode.ToString()), await subRequest.Content.ReadAsStringAsync());
                return _subscriptionIds;
            }

            var data = await JsonDocument.ParseAsync(await subRequest.Content.ReadAsStreamAsync());
            var subscriptionArray = data.RootElement.GetProperty("value").EnumerateArray();
            var subscriptions = subscriptionArray.Select(x => x.GetProperty("subscriptionId").ToString());

            _log.LogTrace($"Got subscription IDs: {string.Join(',', subscriptions)}");
            _subscriptionIds.AddRange(subscriptions);

            return _subscriptionIds;
        }

        private async Task<IEnumerable<ResourceSearchResult>> QueryResourceGraph(string queryText)
        {
            var subscriptions = await FindAccessibleSubscriptions();
            var graphClient = new ResourceGraphClient(new Microsoft.Rest.TokenCredentials(await _tokenProvider.GetAccessTokenAsync(_managementAzureAdResourceId)));
            var query = await graphClient.ResourcesAsync(new QueryRequest(subscriptions.ToList(), queryText));

            var resources = new List<ResourceSearchResult>();
            // the ResourceGraphClient uses Newtonsoft under the hood
            if (((dynamic)query.Data).rows is Newtonsoft.Json.Linq.JArray j)
            {
                resources.AddRange(
                    j.Select(x => new ResourceSearchResult()
                    {
                        // I'm sure there is a better way here - looking at the columns property, for example, 
                        // to find the position of the column in the row we're interested in - follows query order
                        // so for now, 0 & 1
                        ResourceId = x.ElementAt(0).ToString(),
                        SubscriptionId = x.ElementAt(1).ToString()
                    }));
            }

            return resources;
        }

        public async Task<IEnumerable<ResourceSearchResult>> ScanForUntaggedResources()
        {
            var untaggedQuery = @"resourcecontainers | where (isnull(tags.['expires'])) and 
                                                       type == 'microsoft.resources/subscriptions/resourcegroups'
                                                     | project name, subscriptionId, id";
            return await QueryResourceGraph(untaggedQuery);
        }

        public async Task<IEnumerable<ResourceSearchResult>> ScanForExpiredResources()
        {
            var expiredQuery = @"resourcecontainers | where (!isnull(tags.['expires'])) 
                                                      and type == 'microsoft.resources/subscriptions/resourcegroups'
                                                      and todatetime(tags['expires']) < now()
                                                    | project name, subscriptionId, id";
            return await QueryResourceGraph(expiredQuery);
        }

        public async Task<IEnumerable<ResourceSearchResult>> ScanForExpiredResources(DateTimeOffset expirationDate)
        {
            // and todatetime(tags.['expires']) < now() + 14d
            var expiredQuery = $@"resourcecontainers | where (!isnull(tags.['expires'])) 
                                                       and type == 'microsoft.resources/subscriptions/resourcegroups'
                                                       and todatetime(tags['expires']) < todatetime({expirationDate})
                                                     | project name, subscriptionId, id";
            return await QueryResourceGraph(expiredQuery);
        }

        public async Task<IEnumerable<ResourceSearchResult>> ScanForExpiredResources(string kustoDateExpression)
        {
            // e.g., and todatetime(tags.['expires']) < now() + 3d
            var expiredQuery = $@"resourcecontainers | where (!isnull(tags.['expires'])) 
                                                       and type == 'microsoft.resources/subscriptions/resourcegroups'
                                                       and todatetime(tags['expires']) < {kustoDateExpression}
                                                     | project name, subscriptionId, id";
            return await QueryResourceGraph(expiredQuery);
        }
    }
}
 No newline at end of file

b1bd7cab21b3f1ad7ca5cef17b74d862c084573d

what-if? --> log deletion, but don't execute

what-if? --> log deletion, but don't execute

connect to azure

// todo: what-if? --> log deletion, but don't execute

using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;

using Azure.Identity;
using Azure.ResourceManager.Resources;

using Microsoft.Azure.Services.AppAuthentication;
using azman_v2.Model;

namespace azman_v2
{
    public class AzureResourceManagementService : IResourceManagementService
    {
        private readonly HttpClient _httpClient;
        private readonly ILogger<AzureResourceManagementService> _log;
        public AzureResourceManagementService(IHttpClientFactory httpFactory, ILoggerFactory loggerFactory)
        {
            _httpClient = httpFactory.CreateClient();
            _log = loggerFactory.CreateLogger<AzureResourceManagementService>();
        }

        public TaggingRequestModel? ProcessAlert(dynamic alert)
        {
            if (!string.Equals(alert.data.context.activityLog.status.ToString(), "Succeeded", StringComparison.OrdinalIgnoreCase) ||
                !string.Equals(alert.data.context.activityLog.subStatus.ToString(), "Created", StringComparison.OrdinalIgnoreCase))
            {
                _log.LogTrace($"status: {alert.data.context.activityLog.status}");
                _log.LogTrace($"subStatus: {alert.data.context.activityLog.subStatus}");
                //return new OkResult(); // return 200, we're done here since it hasn't succeeded yet
                return null;
            }

            _log.LogTrace($"{alert.data.context.activityLog.resourceGroupName}");

            // handles queueing up new resource groups to be tagged
            return new TaggingRequestModel(
                groupName: alert.data.context.activityLog.resourceGroupName,
                user: alert.data.context.activityLog.caller,
                subscriptionId: alert.data.context.activityLog.subscriptionId,
                created: alert.data.context.activityLog.eventTimestamp
            );
        }

        public async Task TagResource(TaggingRequestModel request)
        {
            // connect to azure
            var resourceManagerClient = new ResourcesManagementClient(request.SubscriptionId, new DefaultAzureCredential());
            var resourceGroupRequest = await resourceManagerClient.ResourceGroups.GetAsync(request.ResourceGroupName);

            if (resourceGroupRequest == null) return;
            var resourceGroup = resourceGroupRequest.Value;

            // todo: move this as default to configuration
            var newDate = request.DateCreated.AddDays(30).Date;

            resourceGroup.Tags.TryAdd("expires", newDate.ToString("yyyy-MM-dd"));
            resourceGroup.Tags.TryAdd("tagged-by", "thrazman");
            resourceGroup.Tags.TryAdd("owner", request.CreatedByUser);

            // service-msft-prod-azman-main-compute
            var groupNamePieces = resourceGroup.Name.Split('-');
            if (groupNamePieces.Count() >= 3)
            {
                resourceGroup.Tags.TryAdd("function", groupNamePieces[0]); // service
                resourceGroup.Tags.TryAdd("customer", groupNamePieces[1]); // msft
                resourceGroup.Tags.TryAdd("env", groupNamePieces[2]); // prod
                resourceGroup.Tags.TryAdd("project", string.Join('-', groupNamePieces.Skip(3))); //azman-main-compute
            }
            await resourceManagerClient.ResourceGroups.CreateOrUpdateAsync(resourceGroup.Name, resourceGroup);
        }

        // todo: explore changes required for using resource ID for _any_ resource
        public async Task DeleteResource(string subscriptionId, string resourceGroupName)
        {
            // todo: what-if? --> log deletion, but don't execute
            // connect to azure
            _log.LogInformation($"Request to delete {resourceGroupName} from subscription {subscriptionId}");
            var resourceManagerClient = new ResourcesManagementClient(subscriptionId, new DefaultAzureCredential());
            await resourceManagerClient.ResourceGroups.StartDeleteAsync(resourceGroupName);
        }

        public async Task<string> ExportResourceGroupTemplateByName(string subscriptionId, string groupName)
        {
            // todo: tweak based on output and ease of re-deploy
            var resourceManagerClient = new ResourcesManagementClient(subscriptionId, new DefaultAzureCredential());
            var exportedTemplate = await resourceManagerClient.ResourceGroups.StartExportTemplateAsync(groupName,
                new Azure.ResourceManager.Resources.Models.ExportTemplateRequest());
            if (exportedTemplate.HasValue) return (string)exportedTemplate.Value.Template; // todo: y tho?
            return string.Empty;
        }
    }
}
 No newline at end of file
ew file mode 100644
ndex 0000000..2336ca1
++ b/IResourceManagementService.cs

39eac02d018fd2d61d8c130fa9614832f21b6bd1

investigate self-hosted github action runner

A self-hosted github action runner would let us deploy via MSI, meaning no static credentials to rotate/manage in github

Deploy via self-hosted only on merge
Use cloud runners for PRs

this is required but read-only? hmm

this is required but read-only? hmm

new Azure.ResourceManager.Resources.Models.ExportTemplateRequest() { Resources = new[] { "*" } });

new Azure.ResourceManager.Resources.Models.ExportTemplateRequest() { });

if (exportedTemplate.HasValue) return (string)exportedTemplate.Value.Template; // todo: y tho?

// // todo: this is required but read-only? hmm

            // var resourceManagerClient = new ResourcesManagementClient(subscriptionId, _tokenCredential);
            // var exportedTemplate = await resourceManagerClient.ResourceGroups.StartExportTemplateAsync(groupName,
            // // todo: this is required but read-only? hmm
            // new Azure.ResourceManager.Resources.Models.ExportTemplateRequest() { Resources = new[] { "*" } });
            // new Azure.ResourceManager.Resources.Models.ExportTemplateRequest() { });
            // if (exportedTemplate.HasValue) return (string)exportedTemplate.Value.Template; // todo: y tho?
            // return string.Empty;
        }
    }
}
 No newline at end of file
ndex 9181db4..9054115 100644
++ b/Functions.cs

39487691903813505a6bd5a4a56ed50f43a731bb

what-if? --> log deletion, but don't execute

what-if? --> log deletion, but don't execute

connect to azure

// todo: what-if? --> log deletion, but don't execute


        // todo: explore changes required for using resource ID for _any_ resource
        public async Task DeleteResource(string subscriptionId, string resourceGroupName)
        {
            // todo: what-if? --> log deletion, but don't execute
            // connect to azure
            _log.LogInformation($"Request to delete {resourceGroupName} from subscription {subscriptionId}");
            var resourceManagerClient = new ResourcesManagementClient(subscriptionId, new DefaultAzureCredential());

4337719531cd61e4df28323998c14d7297ddba34

hack for testing until configurable subscription l...

hack for testing until configurable subscription list is available

// todo: hack for testing until configurable subscription list is available

using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Azure.Management.ResourceGraph;
using Microsoft.Azure.Management.ResourceGraph.Models;

namespace azman_v2
{
    public class Scanner : IScanner
    {
        private readonly ITokenProvider _tokenProvider;
        private readonly HttpClient _httpClient;
        private readonly ILogger<Scanner> _log;

        // todo: configuration for national clouds, e.g., https://management.chinacloudapi.cn
        private readonly string _managementEndpoint = "https://management.azure.com/";
        private readonly string _managementAzureAdResourceId = "https://management.azure.com/";
        // todo: configuration to allow/deny specific subscriptions
        private readonly List<string> _subscriptionIds;

        public Scanner(ITokenProvider tokenProvider, IHttpClientFactory httpFactory, ILoggerFactory loggerFactory)
        {
            _tokenProvider = tokenProvider;
            _httpClient = httpFactory.CreateClient();
            _log = loggerFactory.CreateLogger<Scanner>();
            _subscriptionIds = new List<string>();
        }

        private async Task<IEnumerable<string>> FindAccessibleSubscriptions(bool forceRefresh = false)
        {
            // todo: hack for testing until configurable subscription list is available
            _subscriptionIds.Add("e7048bdb-835c-440f-9304-aa4171382839");
            if (!forceRefresh || _subscriptionIds.Any())
            {
                return _subscriptionIds;
            }

            _log.LogTrace($"Getting subscription list starting at ${DateTime.UtcNow}");
            _log.LogTrace($"Getting access token for ${_managementAzureAdResourceId}");

            var token = await _tokenProvider.GetAccessTokenAsync(_managementAzureAdResourceId, false);
            _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);

            // resource graph is expecting an array of subscriptions, so get the subscription list first
            var subRequest = await _httpClient.GetAsync($"{_managementEndpoint}subscriptions?api-version=2020-01-01");
            if (!subRequest.IsSuccessStatusCode)
            {
                _log.LogError(new EventId((int)subRequest.StatusCode, subRequest.StatusCode.ToString()), await subRequest.Content.ReadAsStringAsync());
                return _subscriptionIds;
            }

            var data = await JsonDocument.ParseAsync(await subRequest.Content.ReadAsStreamAsync());
            var subscriptionArray = data.RootElement.GetProperty("value").EnumerateArray();
            var subscriptions = subscriptionArray.Select(x => x.GetProperty("subscriptionId").ToString());

            _log.LogTrace($"Got subscription IDs: {string.Join(',', subscriptions)}");
            _subscriptionIds.AddRange(subscriptions);

            return _subscriptionIds;
        }

        private async Task<IEnumerable<ResourceSearchResult>> QueryResourceGraph(string queryText)
        {
            var subscriptions = await FindAccessibleSubscriptions();
            var graphClient = new ResourceGraphClient(new Microsoft.Rest.TokenCredentials(await _tokenProvider.GetAccessTokenAsync(_managementAzureAdResourceId)));
            var query = await graphClient.ResourcesAsync(new QueryRequest(subscriptions.ToList(), queryText));

            var resources = new List<ResourceSearchResult>();
            // the ResourceGraphClient uses Newtonsoft under the hood
            if (((dynamic)query.Data).rows is Newtonsoft.Json.Linq.JArray j)
            {
                resources.AddRange(
                    j.Select(x => new ResourceSearchResult()
                    {
                        // I'm sure there is a better way here - looking at the columns property, for example, 
                        // to find the position of the column in the row we're interested in - follows query order
                        // so for now, 0 & 1
                        ResourceId = x.ElementAt(0).ToString(),
                        SubscriptionId = x.ElementAt(1).ToString()
                    }));
            }

            return resources;
        }

        public async Task<IEnumerable<ResourceSearchResult>> ScanForUntaggedResources()
        {
            var untaggedQuery = @"resourcecontainers | where (isnull(tags.['expires'])) and 
                                                       type == 'microsoft.resources/subscriptions/resourcegroups'
                                                     | project name, subscriptionId, id";
            return await QueryResourceGraph(untaggedQuery);
        }

        public async Task<IEnumerable<ResourceSearchResult>> ScanForExpiredResources()
        {
            var expiredQuery = @"resourcecontainers | where (!isnull(tags.['expires'])) 
                                                      and type == 'microsoft.resources/subscriptions/resourcegroups'
                                                      and todatetime(tags['expires']) < now()
                                                    | project name, subscriptionId, id";
            return await QueryResourceGraph(expiredQuery);
        }

        public async Task<IEnumerable<ResourceSearchResult>> ScanForExpiredResources(DateTimeOffset expirationDate)
        {
            // and todatetime(tags.['expires']) < now() + 14d
            var expiredQuery = $@"resourcecontainers | where (!isnull(tags.['expires'])) 
                                                       and type == 'microsoft.resources/subscriptions/resourcegroups'
                                                       and todatetime(tags['expires']) < todatetime({expirationDate})
                                                     | project name, subscriptionId, id";
            return await QueryResourceGraph(expiredQuery);
        }

        public async Task<IEnumerable<ResourceSearchResult>> ScanForExpiredResources(string kustoDateExpression)
        {
            // e.g., and todatetime(tags.['expires']) < now() + 3d
            var expiredQuery = $@"resourcecontainers | where (!isnull(tags.['expires'])) 
                                                       and type == 'microsoft.resources/subscriptions/resourcegroups'
                                                       and todatetime(tags['expires']) < {kustoDateExpression}
                                                     | project name, subscriptionId, id";
            return await QueryResourceGraph(expiredQuery);
        }
    }
}
 No newline at end of file

02f7c572919e1cccc7b77442c5e743720532e0f0

configuration to allow/deny specific subscriptions

configuration to allow/deny specific subscriptions

// todo: configuration to allow/deny specific subscriptions


        // todo: configuration for national clouds, e.g., https://management.chinacloudapi.cn
        private readonly string _managementEndpoint = "https://management.azure.com/";
        private readonly string _managementAzureAdResourceId = "https://management.azure.com/";
        // todo: configuration to allow/deny specific subscriptions
        private readonly List<string> _subscriptionIds;

        public Scanner(ITokenProvider tokenProvider, IHttpClientFactory httpFactory, ILoggerFactory loggerFactory)

5e4120d00b8582357e22961f6a01f2d17d72ed9c

move this as default to configuration

move this as default to configuration

// todo: move this as default to configuration

            var resourceGroup = resourceGroupRequest.Value;

            // todo: move this as default to configuration
            var newDate = request.DateCreated.AddDays(30).Date;

            resourceGroup.Tags.TryAdd("expires", newDate.ToString("yyyy-MM-dd"));

b6059d328e2aa1a610b8d6a0eeb7d5e8327f90a6

y tho?

y tho?

if (exportedTemplate.HasValue) return (string)exportedTemplate.Value.Template; // todo: y tho?

using Microsoft.Extensions.Logging;

using Azure.Identity;
using Azure.ResourceManager.Resources;

using Microsoft.Azure.Services.AppAuthentication;
using azman_v2.Model;

namespace azman_v2
{
    public class AzureResourceManagementService : IResourceManagementService
    {
        private readonly HttpClient _httpClient;
        private readonly ILogger<AzureResourceManagementService> _log;
        public AzureResourceManagementService(IHttpClientFactory httpFactory, ILoggerFactory loggerFactory)
        {
            _httpClient = httpFactory.CreateClient();
            _log = loggerFactory.CreateLogger<AzureResourceManagementService>();
        }

        public TaggingRequestModel? ProcessAlert(dynamic alert)
        {
            if (!string.Equals(alert.data.context.activityLog.status.ToString(), "Succeeded", StringComparison.OrdinalIgnoreCase) ||
                !string.Equals(alert.data.context.activityLog.subStatus.ToString(), "Created", StringComparison.OrdinalIgnoreCase))
            {
                _log.LogTrace($"status: {alert.data.context.activityLog.status}");
                _log.LogTrace($"subStatus: {alert.data.context.activityLog.subStatus}");
                //return new OkResult(); // return 200, we're done here since it hasn't succeeded yet
                return null;
            }

            _log.LogTrace($"{alert.data.context.activityLog.resourceGroupName}");

            // handles queueing up new resource groups to be tagged
            return new TaggingRequestModel(
                groupName: alert.data.context.activityLog.resourceGroupName,
                user: alert.data.context.activityLog.caller,
                subscriptionId: alert.data.context.activityLog.subscriptionId,
                created: alert.data.context.activityLog.eventTimestamp
            );
        }

        public async Task TagResource(TaggingRequestModel request)
        {
            // connect to azure
            var resourceManagerClient = new ResourcesManagementClient(request.SubscriptionId, new DefaultAzureCredential());
            var resourceGroupRequest = await resourceManagerClient.ResourceGroups.GetAsync(request.ResourceGroupName);

            if (resourceGroupRequest == null) return;
            var resourceGroup = resourceGroupRequest.Value;

            // todo: move this as default to configuration
            var newDate = request.DateCreated.AddDays(30).Date;

            resourceGroup.Tags.TryAdd("expires", newDate.ToString("yyyy-MM-dd"));
            resourceGroup.Tags.TryAdd("tagged-by", "thrazman");
            resourceGroup.Tags.TryAdd("owner", request.CreatedByUser);

            // service-msft-prod-azman-main-compute
            var groupNamePieces = resourceGroup.Name.Split('-');
            if (groupNamePieces.Count() >= 3)
            {
                resourceGroup.Tags.TryAdd("function", groupNamePieces[0]); // service
                resourceGroup.Tags.TryAdd("customer", groupNamePieces[1]); // msft
                resourceGroup.Tags.TryAdd("env", groupNamePieces[2]); // prod
                resourceGroup.Tags.TryAdd("project", string.Join('-', groupNamePieces.Skip(3))); //azman-main-compute
            }
            await resourceManagerClient.ResourceGroups.CreateOrUpdateAsync(resourceGroup.Name, resourceGroup);
        }

        // todo: explore changes required for using resource ID for _any_ resource
        public async Task DeleteResource(string subscriptionId, string resourceGroupName)
        {
            // todo: what-if? --> log deletion, but don't execute
            // connect to azure
            _log.LogInformation($"Request to delete {resourceGroupName} from subscription {subscriptionId}");
            var resourceManagerClient = new ResourcesManagementClient(subscriptionId, new DefaultAzureCredential());
            await resourceManagerClient.ResourceGroups.StartDeleteAsync(resourceGroupName);
        }

        public async Task<string> ExportResourceGroupTemplateByName(string subscriptionId, string groupName)
        {
            // todo: tweak based on output and ease of re-deploy
            var resourceManagerClient = new ResourcesManagementClient(subscriptionId, new DefaultAzureCredential());
            var exportedTemplate = await resourceManagerClient.ResourceGroups.StartExportTemplateAsync(groupName,
                new Azure.ResourceManager.Resources.Models.ExportTemplateRequest());
            if (exportedTemplate.HasValue) return (string)exportedTemplate.Value.Template; // todo: y tho?
            return string.Empty;
        }
    }
}
 No newline at end of file
ew file mode 100644
ndex 0000000..2336ca1
++ b/IResourceManagementService.cs

fbd94632e5078c53f67ec03bbee3e5b285809a9b

explore changes required for using resource ID for...

explore changes required for using resource ID for any resource

// todo: explore changes required for using resource ID for _any_ resource

using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;

using Azure.Identity;
using Azure.ResourceManager.Resources;

using Microsoft.Azure.Services.AppAuthentication;
using azman_v2.Model;

namespace azman_v2
{
    public class AzureResourceManagementService : IResourceManagementService
    {
        private readonly HttpClient _httpClient;
        private readonly ILogger<AzureResourceManagementService> _log;
        public AzureResourceManagementService(IHttpClientFactory httpFactory, ILoggerFactory loggerFactory)
        {
            _httpClient = httpFactory.CreateClient();
            _log = loggerFactory.CreateLogger<AzureResourceManagementService>();
        }

        public TaggingRequestModel? ProcessAlert(dynamic alert)
        {
            if (!string.Equals(alert.data.context.activityLog.status.ToString(), "Succeeded", StringComparison.OrdinalIgnoreCase) ||
                !string.Equals(alert.data.context.activityLog.subStatus.ToString(), "Created", StringComparison.OrdinalIgnoreCase))
            {
                _log.LogTrace($"status: {alert.data.context.activityLog.status}");
                _log.LogTrace($"subStatus: {alert.data.context.activityLog.subStatus}");
                //return new OkResult(); // return 200, we're done here since it hasn't succeeded yet
                return null;
            }

            _log.LogTrace($"{alert.data.context.activityLog.resourceGroupName}");

            // handles queueing up new resource groups to be tagged
            return new TaggingRequestModel(
                groupName: alert.data.context.activityLog.resourceGroupName,
                user: alert.data.context.activityLog.caller,
                subscriptionId: alert.data.context.activityLog.subscriptionId,
                created: alert.data.context.activityLog.eventTimestamp
            );
        }

        public async Task TagResource(TaggingRequestModel request)
        {
            // connect to azure
            var resourceManagerClient = new ResourcesManagementClient(request.SubscriptionId, new DefaultAzureCredential());
            var resourceGroupRequest = await resourceManagerClient.ResourceGroups.GetAsync(request.ResourceGroupName);

            if (resourceGroupRequest == null) return;
            var resourceGroup = resourceGroupRequest.Value;

            // todo: move this as default to configuration
            var newDate = request.DateCreated.AddDays(30).Date;

            resourceGroup.Tags.TryAdd("expires", newDate.ToString("yyyy-MM-dd"));
            resourceGroup.Tags.TryAdd("tagged-by", "thrazman");
            resourceGroup.Tags.TryAdd("owner", request.CreatedByUser);

            // service-msft-prod-azman-main-compute
            var groupNamePieces = resourceGroup.Name.Split('-');
            if (groupNamePieces.Count() >= 3)
            {
                resourceGroup.Tags.TryAdd("function", groupNamePieces[0]); // service
                resourceGroup.Tags.TryAdd("customer", groupNamePieces[1]); // msft
                resourceGroup.Tags.TryAdd("env", groupNamePieces[2]); // prod
                resourceGroup.Tags.TryAdd("project", string.Join('-', groupNamePieces.Skip(3))); //azman-main-compute
            }
            await resourceManagerClient.ResourceGroups.CreateOrUpdateAsync(resourceGroup.Name, resourceGroup);
        }

        // todo: explore changes required for using resource ID for _any_ resource
        public async Task DeleteResource(string subscriptionId, string resourceGroupName)
        {
            // todo: what-if? --> log deletion, but don't execute
            // connect to azure
            _log.LogInformation($"Request to delete {resourceGroupName} from subscription {subscriptionId}");
            var resourceManagerClient = new ResourcesManagementClient(subscriptionId, new DefaultAzureCredential());
            await resourceManagerClient.ResourceGroups.StartDeleteAsync(resourceGroupName);
        }

        public async Task<string> ExportResourceGroupTemplateByName(string subscriptionId, string groupName)
        {
            // todo: tweak based on output and ease of re-deploy
            var resourceManagerClient = new ResourcesManagementClient(subscriptionId, new DefaultAzureCredential());
            var exportedTemplate = await resourceManagerClient.ResourceGroups.StartExportTemplateAsync(groupName,
                new Azure.ResourceManager.Resources.Models.ExportTemplateRequest());
            if (exportedTemplate.HasValue) return (string)exportedTemplate.Value.Template; // todo: y tho?
            return string.Empty;
        }
    }
}
 No newline at end of file
ew file mode 100644
ndex 0000000..2336ca1
++ b/IResourceManagementService.cs

79d52bd959dc51fbf46989d665e38dedaa17b6ef

handle 202 accepted on export request, sigh

handle 202 accepted on export request, sigh

// todo: handle 202 accepted on export request, sigh

        public async Task<string> ExportResourceGroupTemplateByName(string subscriptionId, string groupName)
        {
            var token = await _tokenProvider.GetAccessTokenAsync(new[] { "https://management.azure.com/" });
            _httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token.Token);
            var body = new StringContent("{'resources':[ '*' ]}", System.Text.Encoding.UTF8, "application/json");

            var request = await _httpClient.PostAsync($"https://management.azure.com/subscriptions/{subscriptionId}/resourcegroups/{groupName}/exportTemplate?api-version=2020-06-01", body);
            if (!request.IsSuccessStatusCode) return string.Empty;

            // todo: handle 202 accepted on export request, sigh

            var templateData = await request.Content.ReadAsStringAsync();
            return templateData;


            // POST https://management.azure.com/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/exportTemplate?api-version=2020-06-01
            // todo: tweak based on output and ease of re-deploy
            // var resourceManagerClient = new ResourcesManagementClient(subscriptionId, _tokenCredential);
            // var exportedTemplate = await resourceManagerClient.ResourceGroups.StartExportTemplateAsync(groupName,
            // todo: this is required but read-only? hmm
            //new Azure.ResourceManager.Resources.Models.ExportTemplateRequest() { Resources = new[] { "*" } });
            // new Azure.ResourceManager.Resources.Models.ExportTemplateRequest() { });
            // if (exportedTemplate.HasValue) return (string)exportedTemplate.Value.Template; // todo: y tho?
            //return string.Empty;
        }
    }
}
 No newline at end of file
ndex 85f3af9..3c4161a 100644
++ b/Functions.cs

bdd7b1a0a68b153437df0535ab6c28a13a58bd67

move this as default to configuration

move this as default to configuration

// todo: move this as default to configuration

using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;

using Azure.Identity;
using Azure.ResourceManager.Resources;

using Microsoft.Azure.Services.AppAuthentication;
using azman_v2.Model;

namespace azman_v2
{
    public class AzureResourceManagementService : IResourceManagementService
    {
        private readonly HttpClient _httpClient;
        private readonly ILogger<AzureResourceManagementService> _log;
        public AzureResourceManagementService(IHttpClientFactory httpFactory, ILoggerFactory loggerFactory)
        {
            _httpClient = httpFactory.CreateClient();
            _log = loggerFactory.CreateLogger<AzureResourceManagementService>();
        }

        public TaggingRequestModel? ProcessAlert(dynamic alert)
        {
            if (!string.Equals(alert.data.context.activityLog.status.ToString(), "Succeeded", StringComparison.OrdinalIgnoreCase) ||
                !string.Equals(alert.data.context.activityLog.subStatus.ToString(), "Created", StringComparison.OrdinalIgnoreCase))
            {
                _log.LogTrace($"status: {alert.data.context.activityLog.status}");
                _log.LogTrace($"subStatus: {alert.data.context.activityLog.subStatus}");
                //return new OkResult(); // return 200, we're done here since it hasn't succeeded yet
                return null;
            }

            _log.LogTrace($"{alert.data.context.activityLog.resourceGroupName}");

            // handles queueing up new resource groups to be tagged
            return new TaggingRequestModel(
                groupName: alert.data.context.activityLog.resourceGroupName,
                user: alert.data.context.activityLog.caller,
                subscriptionId: alert.data.context.activityLog.subscriptionId,
                created: alert.data.context.activityLog.eventTimestamp
            );
        }

        public async Task TagResource(TaggingRequestModel request)
        {
            // connect to azure
            var resourceManagerClient = new ResourcesManagementClient(request.SubscriptionId, new DefaultAzureCredential());
            var resourceGroupRequest = await resourceManagerClient.ResourceGroups.GetAsync(request.ResourceGroupName);

            if (resourceGroupRequest == null) return;
            var resourceGroup = resourceGroupRequest.Value;

            // todo: move this as default to configuration
            var newDate = request.DateCreated.AddDays(30).Date;

            resourceGroup.Tags.TryAdd("expires", newDate.ToString("yyyy-MM-dd"));
            resourceGroup.Tags.TryAdd("tagged-by", "thrazman");
            resourceGroup.Tags.TryAdd("owner", request.CreatedByUser);

            // service-msft-prod-azman-main-compute
            var groupNamePieces = resourceGroup.Name.Split('-');
            if (groupNamePieces.Count() >= 3)
            {
                resourceGroup.Tags.TryAdd("function", groupNamePieces[0]); // service
                resourceGroup.Tags.TryAdd("customer", groupNamePieces[1]); // msft
                resourceGroup.Tags.TryAdd("env", groupNamePieces[2]); // prod
                resourceGroup.Tags.TryAdd("project", string.Join('-', groupNamePieces.Skip(3))); //azman-main-compute
            }
            await resourceManagerClient.ResourceGroups.CreateOrUpdateAsync(resourceGroup.Name, resourceGroup);
        }

        // todo: explore changes required for using resource ID for _any_ resource
        public async Task DeleteResource(string subscriptionId, string resourceGroupName)
        {
            // todo: what-if? --> log deletion, but don't execute
            // connect to azure
            _log.LogInformation($"Request to delete {resourceGroupName} from subscription {subscriptionId}");
            var resourceManagerClient = new ResourcesManagementClient(subscriptionId, new DefaultAzureCredential());
            await resourceManagerClient.ResourceGroups.StartDeleteAsync(resourceGroupName);
        }

        public async Task<string> ExportResourceGroupTemplateByName(string subscriptionId, string groupName)
        {
            // todo: tweak based on output and ease of re-deploy
            var resourceManagerClient = new ResourcesManagementClient(subscriptionId, new DefaultAzureCredential());
            var exportedTemplate = await resourceManagerClient.ResourceGroups.StartExportTemplateAsync(groupName,
                new Azure.ResourceManager.Resources.Models.ExportTemplateRequest());
            if (exportedTemplate.HasValue) return (string)exportedTemplate.Value.Template; // todo: y tho?
            return string.Empty;
        }
    }
}
 No newline at end of file
ew file mode 100644
ndex 0000000..2336ca1
++ b/IResourceManagementService.cs

3d16d3cf2860618cb508f12f96bbb72538c80664

explore changes required for using resource ID for...

explore changes required for using resource ID for any resource

// todo: explore changes required for using resource ID for _any_ resource

        }

        // todo: explore changes required for using resource ID for _any_ resource
        public async Task DeleteResource(string subscriptionId, string resourceGroupName)
        {
            // todo: what-if? --> log deletion, but don't execute
            // connect to azure
            _log.LogInformation($"Request to delete {resourceGroupName} from subscription {subscriptionId}");
            var resourceManagerClient = new ResourcesManagementClient(subscriptionId, new DefaultAzureCredential());

2b7f8e7ed6ed8172856da8fef25639b808e1b40f

configuration to allow/deny specific subscriptions

configuration to allow/deny specific subscriptions

// todo: configuration to allow/deny specific subscriptions

using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Microsoft.Azure.Management.ResourceGraph;
using Microsoft.Azure.Management.ResourceGraph.Models;

namespace azman_v2
{
    public class Scanner : IScanner
    {
        private readonly ITokenProvider _tokenProvider;
        private readonly HttpClient _httpClient;
        private readonly ILogger<Scanner> _log;

        // todo: configuration for national clouds, e.g., https://management.chinacloudapi.cn
        private readonly string _managementEndpoint = "https://management.azure.com/";
        private readonly string _managementAzureAdResourceId = "https://management.azure.com/";
        // todo: configuration to allow/deny specific subscriptions
        private readonly List<string> _subscriptionIds;

        public Scanner(ITokenProvider tokenProvider, IHttpClientFactory httpFactory, ILoggerFactory loggerFactory)
        {
            _tokenProvider = tokenProvider;
            _httpClient = httpFactory.CreateClient();
            _log = loggerFactory.CreateLogger<Scanner>();
            _subscriptionIds = new List<string>();
        }

        private async Task<IEnumerable<string>> FindAccessibleSubscriptions(bool forceRefresh = false)
        {
            // todo: hack for testing until configurable subscription list is available
            _subscriptionIds.Add("e7048bdb-835c-440f-9304-aa4171382839");
            if (!forceRefresh || _subscriptionIds.Any())
            {
                return _subscriptionIds;
            }

            _log.LogTrace($"Getting subscription list starting at ${DateTime.UtcNow}");
            _log.LogTrace($"Getting access token for ${_managementAzureAdResourceId}");

            var token = await _tokenProvider.GetAccessTokenAsync(_managementAzureAdResourceId, false);
            _httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);

            // resource graph is expecting an array of subscriptions, so get the subscription list first
            var subRequest = await _httpClient.GetAsync($"{_managementEndpoint}subscriptions?api-version=2020-01-01");
            if (!subRequest.IsSuccessStatusCode)
            {
                _log.LogError(new EventId((int)subRequest.StatusCode, subRequest.StatusCode.ToString()), await subRequest.Content.ReadAsStringAsync());
                return _subscriptionIds;
            }

            var data = await JsonDocument.ParseAsync(await subRequest.Content.ReadAsStreamAsync());
            var subscriptionArray = data.RootElement.GetProperty("value").EnumerateArray();
            var subscriptions = subscriptionArray.Select(x => x.GetProperty("subscriptionId").ToString());

            _log.LogTrace($"Got subscription IDs: {string.Join(',', subscriptions)}");
            _subscriptionIds.AddRange(subscriptions);

            return _subscriptionIds;
        }

        private async Task<IEnumerable<ResourceSearchResult>> QueryResourceGraph(string queryText)
        {
            var subscriptions = await FindAccessibleSubscriptions();
            var graphClient = new ResourceGraphClient(new Microsoft.Rest.TokenCredentials(await _tokenProvider.GetAccessTokenAsync(_managementAzureAdResourceId)));
            var query = await graphClient.ResourcesAsync(new QueryRequest(subscriptions.ToList(), queryText));

            var resources = new List<ResourceSearchResult>();
            // the ResourceGraphClient uses Newtonsoft under the hood
            if (((dynamic)query.Data).rows is Newtonsoft.Json.Linq.JArray j)
            {
                resources.AddRange(
                    j.Select(x => new ResourceSearchResult()
                    {
                        // I'm sure there is a better way here - looking at the columns property, for example, 
                        // to find the position of the column in the row we're interested in - follows query order
                        // so for now, 0 & 1
                        ResourceId = x.ElementAt(0).ToString(),
                        SubscriptionId = x.ElementAt(1).ToString()
                    }));
            }

            return resources;
        }

        public async Task<IEnumerable<ResourceSearchResult>> ScanForUntaggedResources()
        {
            var untaggedQuery = @"resourcecontainers | where (isnull(tags.['expires'])) and 
                                                       type == 'microsoft.resources/subscriptions/resourcegroups'
                                                     | project name, subscriptionId, id";
            return await QueryResourceGraph(untaggedQuery);
        }

        public async Task<IEnumerable<ResourceSearchResult>> ScanForExpiredResources()
        {
            var expiredQuery = @"resourcecontainers | where (!isnull(tags.['expires'])) 
                                                      and type == 'microsoft.resources/subscriptions/resourcegroups'
                                                      and todatetime(tags['expires']) < now()
                                                    | project name, subscriptionId, id";
            return await QueryResourceGraph(expiredQuery);
        }

        public async Task<IEnumerable<ResourceSearchResult>> ScanForExpiredResources(DateTimeOffset expirationDate)
        {
            // and todatetime(tags.['expires']) < now() + 14d
            var expiredQuery = $@"resourcecontainers | where (!isnull(tags.['expires'])) 
                                                       and type == 'microsoft.resources/subscriptions/resourcegroups'
                                                       and todatetime(tags['expires']) < todatetime({expirationDate})
                                                     | project name, subscriptionId, id";
            return await QueryResourceGraph(expiredQuery);
        }

        public async Task<IEnumerable<ResourceSearchResult>> ScanForExpiredResources(string kustoDateExpression)
        {
            // e.g., and todatetime(tags.['expires']) < now() + 3d
            var expiredQuery = $@"resourcecontainers | where (!isnull(tags.['expires'])) 
                                                       and type == 'microsoft.resources/subscriptions/resourcegroups'
                                                       and todatetime(tags['expires']) < {kustoDateExpression}
                                                     | project name, subscriptionId, id";
            return await QueryResourceGraph(expiredQuery);
        }
    }
}
 No newline at end of file

e4c965ed15d1dd00a5256803313bc416ea8b5042

if location == null or empty, POST to export Uri

if location == null or empty, POST to export Uri

request = await _httpClient.GetAsync(request.Headers.Location ?? exportUri);

}

// // todo: if location == null or empty, POST to export Uri

        public async Task<string> ExportResourceGroupTemplateByName(string subscriptionId, string groupName)
        {
            // var token = await _tokenProvider.GetAccessTokenAsync(new[] { "https://management.azure.com/" });
            // _httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token.Token);
            // var exportUri = new Uri($"https://management.azure.com/subscriptions/{subscriptionId}/resourcegroups/{groupName}/exportTemplate?api-version=2020-06-01");
            // var body = new StringContent("{'resources':[ '*' ]}", System.Text.Encoding.UTF8, "application/json");

            // var request = await _httpClient.PostAsync(exportUri, body);
            // if (!request.IsSuccessStatusCode) return string.Empty;

            // while (request.StatusCode == System.Net.HttpStatusCode.Accepted)
            // {
            //     var delayInSec = 15;
            //     if (request.Headers.RetryAfter.Delta != null)
            //     {
            //         delayInSec = Convert.ToInt32(request.Headers.RetryAfter.Delta.Value.TotalSeconds);
            //     }
            //     await Task.Delay(delayInSec * 1000);
            //     // todo: if location == null or empty, POST to export Uri
            //     request = await _httpClient.GetAsync(request.Headers.Location ?? exportUri);
            // }

            // if (!request.IsSuccessStatusCode) return string.Empty;

            // var templateData = await request.Content.ReadAsStringAsync();
            // return templateData;

            // POST https://management.azure.com/subscriptions/{subscriptionId}/resourcegroups/{resourceGroupName}/exportTemplate?api-version=2020-06-01
            // todo: tweak based on output and ease of re-deploy
            var resourceManagerClient = new ResourcesManagementClient(subscriptionId, _tokenCredential);
            var resourceTypesToExport = new Azure.ResourceManager.Resources.Models.ExportTemplateRequest();
            resourceTypesToExport.Resources.Add("*");
            var exportedTemplate = await resourceManagerClient.ResourceGroups.StartExportTemplateAsync(groupName, resourceTypesToExport);
            if (exportedTemplate.HasValue) return (string)exportedTemplate.Value.Template;
            return string.Empty;
        }
    }
}
 No newline at end of file
ndex 9054115..1e73363 100644
++ b/Functions.cs

ed0f675aab0b47df224508e2297221e56e79e368

configuration for national clouds, e.g., https://m...

configuration for national clouds, e.g., https://management.chinacloudapi.cn

// todo: configuration for national clouds, e.g., https://management.chinacloudapi.cn

        private readonly ILogger<Scanner> _log;

        // todo: configuration for national clouds, e.g., https://management.chinacloudapi.cn
        private readonly string _managementEndpoint = "https://management.azure.com/";
        private readonly string _managementAzureAdResourceId = "https://management.azure.com/";
        // todo: configuration to allow/deny specific subscriptions
        private readonly List<string> _subscriptionIds;

        public Scanner(ITokenProvider tokenProvider, IHttpClientFactory httpFactory, ILoggerFactory loggerFactory)

6555143074d3881821541aa42d6ddaa6e6974db0

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.