src/Analyzer.Core/TemplateAnalyzer.cs (179 lines of code) (raw):
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Microsoft.Azure.Templates.Analyzer.BicepProcessor;
using Microsoft.Azure.Templates.Analyzer.RuleEngines.JsonEngine;
using Microsoft.Azure.Templates.Analyzer.RuleEngines.PowerShellEngine;
using Microsoft.Azure.Templates.Analyzer.TemplateProcessor;
using Microsoft.Azure.Templates.Analyzer.Types;
using Microsoft.Azure.Templates.Analyzer.Utilities;
using Microsoft.Extensions.Logging;
using Microsoft.WindowsAzure.ResourceStack.Common.Extensions;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Microsoft.Azure.Templates.Analyzer.Core
{
/// <summary>
/// This class runs the TemplateAnalyzer logic given the template and parameters passed to it.
/// </summary>
public class TemplateAnalyzer
{
/// <summary>
/// Exception message when error during Bicep template compilation.
/// </summary>
public static readonly string BicepCompileErrorMessage = "Error compiling Bicep template";
private JsonRuleEngine jsonRuleEngine;
private PowerShellRuleEngine powerShellRuleEngine;
private ILogger logger;
/// <summary>
/// Private constructor to enforce using one of the TemplateAnalyzer.Create methods for creating new instances.
/// </summary>
/// <param name="jsonRuleEngine">The <see cref="JsonRuleEngine"/> to use in analyzing templates.</param>
/// <param name="powerShellRuleEngine">The <see cref="PowerShellRuleEngine"/> to use in analyzing templates.</param>
/// <param name="logger">A logger to report errors and debug information</param>
private TemplateAnalyzer(JsonRuleEngine jsonRuleEngine, PowerShellRuleEngine powerShellRuleEngine, ILogger logger)
{
this.jsonRuleEngine = jsonRuleEngine;
this.powerShellRuleEngine = powerShellRuleEngine;
this.logger = logger;
}
/// <summary>
/// Creates a new <see cref="TemplateAnalyzer"/> instance with the default built-in rules.
/// </summary>
/// <param name="includeNonSecurityRules">Whether or not to run also non-security rules against the template.</param>
/// <param name="logger">A logger to report errors and debug information</param>
/// <param name="customJsonRulesPath">An optional custom rules json file path.</param>
/// <param name="includePowerShellRules">Whether or not to run also powershell rules against the template.</param>
/// <returns>A new <see cref="TemplateAnalyzer"/> instance.</returns>
public static TemplateAnalyzer Create(bool includeNonSecurityRules, ILogger logger = null, FileInfo customJsonRulesPath = null, bool includePowerShellRules = true)
{
string rules;
try
{
rules = LoadRules(customJsonRulesPath);
}
catch (Exception e)
{
throw new TemplateAnalyzerException("Failed to read rules.", e);
}
return Create(includeNonSecurityRules: includeNonSecurityRules, includePowerShellRules: includePowerShellRules, rulesJsonAsString: rules, logger: logger);
}
/// <summary>
/// Creates a new <see cref="TemplateAnalyzer"/> instance.
/// </summary>
/// <param name="includeNonSecurityRules">Whether or not to run also non-security rules against the template.</param>
/// <param name="rulesJsonAsString">The rules to evaluate, in JSON string format.</param>
/// <param name="logger">A logger to report errors and debug information</param>
/// <param name="includePowerShellRules">Whether or not to run also powershell rules against the template.</param>
/// <returns>A new <see cref="TemplateAnalyzer"/> instance.</returns>
public static TemplateAnalyzer Create(bool includeNonSecurityRules, bool includePowerShellRules, string rulesJsonAsString, ILogger logger = null)
{
return new TemplateAnalyzer(
JsonRuleEngine.Create(
rulesJsonAsString,
templateContext => templateContext.IsBicep
? new BicepSourceLocationResolver(templateContext)
: new JsonSourceLocationResolver(templateContext),
logger),
includePowerShellRules ? new PowerShellRuleEngine(includeNonSecurityRules, logger) : null,
logger);
}
/// <summary>
/// Runs the TemplateAnalyzer logic given the template and parameters passed to it.
/// </summary>
/// <param name="template">The template contents.</param>
/// <param name="templateFilePath">The template file path. It's needed to analyze Bicep files and to run the PowerShell based rules.</param>
/// <param name="parameters">The parameters for the template.</param>
/// <returns>An enumerable of TemplateAnalyzer evaluations.</returns>
public IEnumerable<IEvaluation> AnalyzeTemplate(string template, string templateFilePath, string parameters = null)
{
if (template == null) throw new ArgumentNullException(nameof(template));
if (templateFilePath == null) throw new ArgumentNullException(nameof(templateFilePath));
// If the template is Bicep, convert to JSON and get source map:
var isBicep = templateFilePath != null && templateFilePath.ToLower().EndsWith(".bicep", StringComparison.OrdinalIgnoreCase);
object bicepMetadata = null;
if (isBicep)
{
try
{
(template, bicepMetadata) = BicepTemplateProcessor.ConvertBicepToJson(templateFilePath);
}
catch (Exception e)
{
throw new TemplateAnalyzerException(BicepCompileErrorMessage, e);
}
}
var templateContext = new TemplateContext
{
OriginalTemplate = null,
ExpandedTemplate = null,
IsMainTemplate = true,
ResourceMappings = null,
TemplateIdentifier = templateFilePath,
IsBicep = isBicep,
BicepMetadata = bicepMetadata,
PathPrefix = "",
ParentContext = null
};
return AnalyzeAllIncludedTemplates(template, parameters, templateFilePath, templateContext, string.Empty);
}
/// <summary>
/// Analyzes ARM templates, recursively going through the nested templates
/// </summary>
/// <param name="populatedTemplate">The ARM Template JSON with inherited parameters, variables, and functions, if applicable</param>
/// <param name="parameters">The parameters for the ARM Template JSON</param>
/// <param name="templateFilePath">The ARM Template file path</param>
/// <param name="parentContext">Template context for the immediate parent template</param>
/// <param name="pathPrefix"> Prefix for resources' path used for line number mapping in nested templates</param>
/// <returns>An enumerable of TemplateAnalyzer evaluations.</returns>
private IEnumerable<IEvaluation> AnalyzeAllIncludedTemplates(string populatedTemplate, string parameters, string templateFilePath, TemplateContext parentContext, string pathPrefix)
{
JToken templatejObject;
var armTemplateProcessor = new ArmTemplateProcessor(populatedTemplate, logger: this.logger);
try
{
templatejObject = armTemplateProcessor.ProcessTemplate(parameters);
}
catch (Exception e)
{
throw new TemplateAnalyzerException("Error while processing template.", e);
}
var templateContext = new TemplateContext
{
OriginalTemplate = JObject.Parse(populatedTemplate),
ExpandedTemplate = templatejObject,
IsMainTemplate = parentContext.OriginalTemplate == null, // Even the top level context will have a parent defined, but it won't represent a processed template
ResourceMappings = armTemplateProcessor.ResourceMappings,
TemplateIdentifier = templateFilePath,
IsBicep = parentContext.IsBicep,
BicepMetadata = parentContext.BicepMetadata,
PathPrefix = pathPrefix,
ParentContext = parentContext
};
try
{
IEnumerable<IEvaluation> evaluations = this.jsonRuleEngine.AnalyzeTemplate(templateContext);
if (this.powerShellRuleEngine is not null)
{
evaluations = evaluations.Concat(this.powerShellRuleEngine.AnalyzeTemplate(templateContext));
}
// Recursively handle nested templates
var jsonTemplate = JObject.Parse(populatedTemplate);
var processedTemplateResources = templatejObject.InsensitiveToken("resources");
for (int i = 0; i < processedTemplateResources.Count(); i++)
{
var currentProcessedResource = processedTemplateResources[i];
if (currentProcessedResource.InsensitiveToken("type")?.ToString().Equals("Microsoft.Resources/deployments", StringComparison.OrdinalIgnoreCase) ?? false)
{
var nestedTemplate = currentProcessedResource.InsensitiveToken("properties.template");
if (nestedTemplate == null)
{
this.logger?.LogWarning($"A linked template was found on: {templateFilePath}, linked templates are currently not supported");
continue;
}
var populatedNestedTemplate = nestedTemplate.DeepClone();
// Check whether scope is set to inner or outer
var scope = currentProcessedResource.InsensitiveToken("properties.expressionEvaluationOptions.scope")?.ToString();
if (scope == null || scope.Equals("outer", StringComparison.OrdinalIgnoreCase))
{
// Variables, parameters and functions inherited from parent template
string functionsKey = populatedNestedTemplate.InsensitiveToken("functions")?.Parent.Path ?? "functions";
string variablesKey = populatedNestedTemplate.InsensitiveToken("variables")?.Parent.Path ?? "variables";
string parametersKey = populatedNestedTemplate.InsensitiveToken("parameters")?.Parent.Path ?? "parameters";
populatedNestedTemplate[functionsKey] = jsonTemplate.InsensitiveToken("functions");
populatedNestedTemplate[variablesKey] = jsonTemplate.InsensitiveToken("variables");
populatedNestedTemplate[parametersKey] = jsonTemplate.InsensitiveToken("parameters");
}
else // scope is inner
{
// Pass variables and functions to child template
(populatedNestedTemplate.InsensitiveToken("variables") as JObject)?.Merge(currentProcessedResource.InsensitiveToken("properties.variables"));
(populatedNestedTemplate.InsensitiveToken("functions") as JObject)?.Merge(currentProcessedResource.InsensitiveToken("properties.functions)"));
// Pass parameters to child template as the 'parameters' argument
var parametersToPass = currentProcessedResource.InsensitiveToken("properties.parameters");
if (parametersToPass != null)
{
parametersToPass["parameters"] = parametersToPass;
parametersToPass["$schema"] = "https://schema.management.azure.com/schemas/2019-04-01/deploymentParameters.json#";
parametersToPass["contentVersion"] = "1.0.0.0";
parameters = JsonConvert.SerializeObject(parametersToPass);
}
}
string jsonPopulatedNestedTemplate = JsonConvert.SerializeObject(populatedNestedTemplate);
IEnumerable<IEvaluation> result = AnalyzeAllIncludedTemplates(jsonPopulatedNestedTemplate, parameters, templateFilePath, templateContext, nestedTemplate.Path);
evaluations = evaluations.Concat(result);
}
}
return evaluations;
}
catch (Exception e)
{
throw new TemplateAnalyzerException("Error while evaluating rules.", e);
}
}
private static string LoadRules(FileInfo rulesFile)
{
rulesFile ??= new FileInfo(Path.Combine(
Path.GetDirectoryName(AppContext.BaseDirectory),
"Rules/BuiltInRules.json"));
using var fileStream = rulesFile.OpenRead();
using var streamReader = new StreamReader(fileStream);
return streamReader.ReadToEnd();
}
/// <summary>
/// Modifies the rules to run based on values defined in the configuration file.
/// </summary>
/// <param name="configuration">The configuration specifying rule modifications.</param>
public void FilterRules(ConfigurationDefinition configuration)
{
jsonRuleEngine.FilterRules(configuration);
}
}
}