using MarketAlly.AIPlugin; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.Json; using System.Text.RegularExpressions; using System.Threading.Tasks; using System.Xml; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; namespace MarketAlly.AIPlugin.DevOps.Plugins { [AIPlugin("ConfigurationAnalyzer", "Analyzes configuration files for consistency, security, and environment management")] public class ConfigurationAnalyzerPlugin : IAIPlugin { private readonly ILogger _logger; private readonly IDeserializer _yamlDeserializer; public ConfigurationAnalyzerPlugin(ILogger logger = null) { _logger = logger; _yamlDeserializer = new DeserializerBuilder() .WithNamingConvention(HyphenatedNamingConvention.Instance) .IgnoreUnmatchedProperties() .Build(); } [AIParameter("Full path to the configuration directory", required: true)] public string ConfigDirectory { get; set; } [AIParameter("Configuration file patterns to analyze", required: false)] public string FilePatterns { get; set; } = "*.json,*.yaml,*.yml,*.xml,*.config"; [AIParameter("Check for configuration drift between environments", required: false)] public bool CheckDrift { get; set; } = true; [AIParameter("Validate environment-specific settings", required: false)] public bool ValidateEnvironments { get; set; } = true; [AIParameter("Check for missing or deprecated settings", required: false)] public bool CheckSettings { get; set; } = true; [AIParameter("Generate configuration documentation", required: false)] public bool GenerateDocumentation { get; set; } = false; public IReadOnlyDictionary SupportedParameters => new Dictionary { ["configDirectory"] = typeof(string), ["filePatterns"] = typeof(string), ["checkDrift"] = typeof(bool), ["validateEnvironments"] = typeof(bool), ["checkSettings"] = typeof(bool), ["generateDocumentation"] = typeof(bool) }; public async Task ExecuteAsync(IReadOnlyDictionary parameters) { try { _logger?.LogInformation("ConfigurationAnalyzer plugin executing"); // Extract parameters string configDirectory = parameters["configDirectory"].ToString(); string filePatterns = parameters.TryGetValue("filePatterns", out var patternsObj) ? patternsObj.ToString() : "*.json,*.yaml,*.yml,*.xml,*.config"; bool checkDrift = parameters.TryGetValue("checkDrift", out var driftObj) && Convert.ToBoolean(driftObj); bool validateEnvironments = parameters.TryGetValue("validateEnvironments", out var envObj) && Convert.ToBoolean(envObj); bool checkSettings = parameters.TryGetValue("checkSettings", out var settingsObj) && Convert.ToBoolean(settingsObj); bool generateDocumentation = parameters.TryGetValue("generateDocumentation", out var docObj) && Convert.ToBoolean(docObj); // Validate directory exists if (!Directory.Exists(configDirectory)) { return new AIPluginResult( new DirectoryNotFoundException($"Configuration directory not found: {configDirectory}"), "Configuration directory not found" ); } // Discover configuration files var configFiles = await DiscoverConfigurationFilesAsync(configDirectory, filePatterns); if (!configFiles.Any()) { return new AIPluginResult( new InvalidOperationException("No configuration files found"), "No configuration files found" ); } var analysisResult = new ConfigurationAnalysisResult { ConfigDirectory = configDirectory, FilesAnalyzed = configFiles.Count }; // Parse all configuration files var configData = new Dictionary(); foreach (var file in configFiles) { try { var fileData = await ParseConfigurationFileAsync(file); configData[file] = fileData; } catch (Exception ex) { _logger?.LogWarning(ex, "Failed to parse configuration file: {FilePath}", file); analysisResult.ConfigurationIssues.Add(new ConfigurationIssue { Severity = "Error", Category = "Parsing", Issue = $"Failed to parse configuration file: {ex.Message}", Location = file, Recommendation = "Verify file format and syntax" }); } } // Perform analysis if (checkDrift && configData.Count > 1) { AnalyzeConfigurationDrift(configData, analysisResult); } if (validateEnvironments) { AnalyzeEnvironmentSettings(configData, analysisResult); } if (checkSettings) { AnalyzeSettingsConsistency(configData, analysisResult); } // Check for security issues AnalyzeSecurityIssues(configData, analysisResult); // Generate documentation if requested string documentation = null; if (generateDocumentation) { documentation = GenerateConfigurationDocumentation(configData, analysisResult); } var result = new { Message = "Configuration analysis completed", ConfigDirectory = configDirectory, FilesAnalyzed = analysisResult.FilesAnalyzed, ConfigurationDrift = analysisResult.ConfigurationDrift, MissingSettings = analysisResult.MissingSettings, DeprecatedSettings = analysisResult.DeprecatedSettings, EnvironmentIssues = analysisResult.EnvironmentIssues, ConfigurationIssues = analysisResult.ConfigurationIssues, SecurityIssues = analysisResult.SecurityIssues, Documentation = documentation, Summary = new { TotalIssues = analysisResult.ConfigurationIssues.Count + analysisResult.SecurityIssues.Count, DriftDetected = analysisResult.ConfigurationDrift.Any(), MissingSettingsCount = analysisResult.MissingSettings.Count, SecurityIssuesCount = analysisResult.SecurityIssues.Count, OverallScore = CalculateOverallScore(analysisResult) } }; _logger?.LogInformation("Configuration analysis completed. Found {TotalIssues} issues, {DriftItems} drift items, {SecurityIssues} security issues", result.Summary.TotalIssues, analysisResult.ConfigurationDrift.Count, result.Summary.SecurityIssuesCount); return new AIPluginResult(result); } catch (Exception ex) { _logger?.LogError(ex, "Failed to analyze configuration files"); return new AIPluginResult(ex, "Failed to analyze configuration files"); } } private async Task> DiscoverConfigurationFilesAsync(string directory, string patterns) { var files = new List(); var patternList = patterns.Split(',', StringSplitOptions.RemoveEmptyEntries) .Select(p => p.Trim()) .ToArray(); foreach (var pattern in patternList) { try { files.AddRange(Directory.GetFiles(directory, pattern, SearchOption.AllDirectories)); } catch (Exception ex) { _logger?.LogWarning(ex, "Failed to search for pattern {Pattern} in {Directory}", pattern, directory); } } // Remove duplicates and sort return files.Distinct().OrderBy(f => f).ToList(); } private async Task ParseConfigurationFileAsync(string filePath) { var fileData = new ConfigurationFileData { FilePath = filePath, FileName = Path.GetFileName(filePath), FileType = DetermineFileType(filePath) }; var content = await File.ReadAllTextAsync(filePath); fileData.RawContent = content; // Parse based on file type switch (fileData.FileType) { case ConfigurationFileType.Json: fileData.ParsedData = JsonSerializer.Deserialize>(content); break; case ConfigurationFileType.Yaml: fileData.ParsedData = _yamlDeserializer.Deserialize>(content); break; case ConfigurationFileType.Xml: fileData.ParsedData = ParseXmlToKeyValuePairs(content); break; case ConfigurationFileType.Properties: fileData.ParsedData = ParsePropertiesFile(content); break; default: fileData.ParsedData = new Dictionary(); break; } // Extract environment indicator fileData.Environment = DetermineEnvironment(filePath); // Flatten the configuration for easier comparison fileData.FlattenedKeys = FlattenConfiguration(fileData.ParsedData); return fileData; } private ConfigurationFileType DetermineFileType(string filePath) { var extension = Path.GetExtension(filePath).ToLower(); return extension switch { ".json" => ConfigurationFileType.Json, ".yaml" or ".yml" => ConfigurationFileType.Yaml, ".xml" or ".config" => ConfigurationFileType.Xml, ".properties" or ".env" => ConfigurationFileType.Properties, _ => ConfigurationFileType.Unknown }; } private string DetermineEnvironment(string filePath) { var fileName = Path.GetFileNameWithoutExtension(filePath).ToLower(); // Common environment patterns if (fileName.Contains("dev") || fileName.Contains("development")) return "Development"; if (fileName.Contains("test") || fileName.Contains("testing")) return "Testing"; if (fileName.Contains("stage") || fileName.Contains("staging")) return "Staging"; if (fileName.Contains("prod") || fileName.Contains("production")) return "Production"; if (fileName.Contains("local")) return "Local"; // Check for appsettings.{env}.json pattern var match = Regex.Match(fileName, @"appsettings\.(\w+)"); if (match.Success) { return match.Groups[1].Value; } return "Unknown"; } private Dictionary ParseXmlToKeyValuePairs(string xmlContent) { var result = new Dictionary(); try { var doc = new XmlDocument(); doc.LoadXml(xmlContent); ParseXmlNode(doc.DocumentElement, "", result); } catch (Exception ex) { _logger?.LogWarning(ex, "Failed to parse XML content"); } return result; } private void ParseXmlNode(XmlNode node, string prefix, Dictionary result) { if (node == null) return; var currentPath = string.IsNullOrEmpty(prefix) ? node.Name : $"{prefix}.{node.Name}"; if (node.HasChildNodes && node.ChildNodes.Cast().Any(n => n.NodeType == XmlNodeType.Element)) { foreach (XmlNode child in node.ChildNodes) { if (child.NodeType == XmlNodeType.Element) { ParseXmlNode(child, currentPath, result); } } } else { result[currentPath] = node.InnerText; } // Handle attributes if (node.Attributes != null) { foreach (XmlAttribute attr in node.Attributes) { result[$"{currentPath}@{attr.Name}"] = attr.Value; } } } private Dictionary ParsePropertiesFile(string content) { var result = new Dictionary(); var lines = content.Split('\n', StringSplitOptions.RemoveEmptyEntries); foreach (var line in lines) { var trimmedLine = line.Trim(); if (string.IsNullOrEmpty(trimmedLine) || trimmedLine.StartsWith("#") || trimmedLine.StartsWith("//")) continue; var equalIndex = trimmedLine.IndexOf('='); if (equalIndex > 0) { var key = trimmedLine.Substring(0, equalIndex).Trim(); var value = trimmedLine.Substring(equalIndex + 1).Trim(); result[key] = value; } } return result; } private Dictionary FlattenConfiguration(Dictionary config, string prefix = "") { var flattened = new Dictionary(); foreach (var kvp in config) { var key = string.IsNullOrEmpty(prefix) ? kvp.Key : $"{prefix}.{kvp.Key}"; if (kvp.Value is Dictionary nestedDict) { var nested = FlattenConfiguration(nestedDict, key); foreach (var nestedKvp in nested) { flattened[nestedKvp.Key] = nestedKvp.Value; } } else if (kvp.Value is JsonElement jsonElement && jsonElement.ValueKind == JsonValueKind.Object) { var nestedDict2 = JsonSerializer.Deserialize>(jsonElement.GetRawText()); var nested = FlattenConfiguration(nestedDict2, key); foreach (var nestedKvp in nested) { flattened[nestedKvp.Key] = nestedKvp.Value; } } else { flattened[key] = kvp.Value; } } return flattened; } private void AnalyzeConfigurationDrift(Dictionary configData, ConfigurationAnalysisResult result) { var environments = configData.Values .Where(c => c.Environment != "Unknown") .GroupBy(c => c.Environment) .ToDictionary(g => g.Key, g => g.ToList()); if (environments.Count < 2) return; // Get all unique keys across all environments var allKeys = configData.Values .SelectMany(c => c.FlattenedKeys.Keys) .Distinct() .ToList(); foreach (var key in allKeys) { var environmentValues = new Dictionary(); foreach (var env in environments) { var envFiles = env.Value; var keyValues = envFiles .Where(f => f.FlattenedKeys.ContainsKey(key)) .Select(f => f.FlattenedKeys[key]) .Distinct() .ToList(); if (keyValues.Count == 1) { environmentValues[env.Key] = keyValues.First(); } else if (keyValues.Count > 1) { result.ConfigurationIssues.Add(new ConfigurationIssue { Severity = "Warning", Category = "Inconsistency", Issue = $"Multiple values for key '{key}' in environment '{env.Key}'", Location = string.Join(", ", envFiles.Select(f => f.FilePath)), Recommendation = "Ensure consistent values within the same environment" }); } } // Check for drift between environments if (environmentValues.Count > 1) { var uniqueValues = environmentValues.Values.Distinct().ToList(); if (uniqueValues.Count > 1 && !ShouldAllowDrift(key)) { result.ConfigurationDrift.Add(new ConfigurationDrift { Key = key, EnvironmentValues = environmentValues.ToDictionary(kvp => kvp.Key, kvp => kvp.Value?.ToString()), DriftType = "Value Mismatch", Recommendation = "Review if this configuration should be environment-specific" }); } } // Check for missing keys in environments foreach (var env in environments.Keys) { if (!environmentValues.ContainsKey(env)) { result.MissingSettings.Add(new MissingSetting { Key = key, Environment = env, Severity = "Medium", Recommendation = $"Add configuration for '{key}' in {env} environment" }); } } } } private bool ShouldAllowDrift(string key) { // Keys that are expected to differ between environments var allowedDriftPatterns = new[] { "connectionstrings", "database", "server", "host", "url", "endpoint", "environment", "debug", "logging.level" }; return allowedDriftPatterns.Any(pattern => key.ToLower().Contains(pattern.ToLower())); } private void AnalyzeEnvironmentSettings(Dictionary configData, ConfigurationAnalysisResult result) { foreach (var config in configData.Values) { // Check for hardcoded environment-specific values in non-environment files if (config.Environment == "Unknown") { foreach (var kvp in config.FlattenedKeys) { var value = kvp.Value?.ToString()?.ToLower(); if (!string.IsNullOrEmpty(value)) { if (value.Contains("localhost") || value.Contains("127.0.0.1") || value.Contains("dev") || value.Contains("test")) { result.EnvironmentIssues.Add(new EnvironmentIssue { Issue = "Hardcoded environment-specific value detected", Key = kvp.Key, Value = value, Location = config.FilePath, Recommendation = "Use environment-specific configuration files or environment variables" }); } } } } // Check for missing essential settings var essentialKeys = new[] { "connectionstrings", "logging", "authentication" }; foreach (var essential in essentialKeys) { if (!config.FlattenedKeys.Keys.Any(k => k.ToLower().Contains(essential))) { result.MissingSettings.Add(new MissingSetting { Key = essential, Environment = config.Environment, Severity = "Low", Recommendation = $"Consider adding {essential} configuration" }); } } } } private void AnalyzeSettingsConsistency(Dictionary configData, ConfigurationAnalysisResult result) { var deprecatedPatterns = new[] { "appSettings", // Legacy .NET Framework "system.web", // Legacy ASP.NET "microsoft.aspnet", // Legacy ASP.NET "system.webserver" // Legacy IIS }; foreach (var config in configData.Values) { foreach (var kvp in config.FlattenedKeys) { foreach (var pattern in deprecatedPatterns) { if (kvp.Key.ToLower().Contains(pattern.ToLower())) { result.DeprecatedSettings.Add(new DeprecatedSetting { Key = kvp.Key, Value = kvp.Value?.ToString(), Location = config.FilePath, Reason = $"Uses deprecated configuration pattern: {pattern}", Recommendation = "Migrate to modern configuration patterns" }); } } } } } private void AnalyzeSecurityIssues(Dictionary configData, ConfigurationAnalysisResult result) { var secretPatterns = new[] { @"(?i)(password|pwd|pass|secret|token|key|api[-_]?key)[\s]*[:=][\s]*[""']?[a-zA-Z0-9+/]{8,}[""']?", @"(?i)connectionstring.*password=.*", @"(?i)(aws[-_]?access[-_]?key|aws[-_]?secret)", @"(?i)(github|gitlab)[-_]?token" }; foreach (var config in configData.Values) { foreach (var pattern in secretPatterns) { var matches = Regex.Matches(config.RawContent, pattern); foreach (Match match in matches) { result.SecurityIssues.Add(new ConfigurationSecurityIssue { Severity = "High", Issue = "Potential hardcoded secret detected", Location = config.FilePath, Recommendation = "Use environment variables, key vaults, or secure configuration providers", Pattern = match.Value.Substring(0, Math.Min(50, match.Value.Length)) + "..." }); } } // Check for insecure settings foreach (var kvp in config.FlattenedKeys) { var key = kvp.Key.ToLower(); var value = kvp.Value?.ToString()?.ToLower(); if (key.Contains("ssl") && value == "false") { result.SecurityIssues.Add(new ConfigurationSecurityIssue { Severity = "Medium", Issue = "SSL/TLS disabled", Location = config.FilePath, Recommendation = "Enable SSL/TLS for secure communication", Pattern = $"{kvp.Key} = {kvp.Value}" }); } if (key.Contains("debug") && value == "true" && config.Environment == "Production") { result.SecurityIssues.Add(new ConfigurationSecurityIssue { Severity = "Medium", Issue = "Debug mode enabled in production", Location = config.FilePath, Recommendation = "Disable debug mode in production environments", Pattern = $"{kvp.Key} = {kvp.Value}" }); } } } } private string GenerateConfigurationDocumentation(Dictionary configData, ConfigurationAnalysisResult result) { var doc = new System.Text.StringBuilder(); doc.AppendLine("# Configuration Analysis Documentation"); doc.AppendLine($"Generated on: {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC\n"); doc.AppendLine("## Configuration Files"); foreach (var config in configData.Values.OrderBy(c => c.Environment).ThenBy(c => c.FileName)) { doc.AppendLine($"### {config.FileName}"); doc.AppendLine($"- **Type**: {config.FileType}"); doc.AppendLine($"- **Environment**: {config.Environment}"); doc.AppendLine($"- **Path**: `{config.FilePath}`"); doc.AppendLine($"- **Settings Count**: {config.FlattenedKeys.Count}"); doc.AppendLine(); } if (result.ConfigurationDrift.Any()) { doc.AppendLine("## Configuration Drift"); foreach (var drift in result.ConfigurationDrift) { doc.AppendLine($"### {drift.Key}"); doc.AppendLine($"**Type**: {drift.DriftType}"); doc.AppendLine("**Values by Environment**:"); foreach (var env in drift.EnvironmentValues) { doc.AppendLine($"- {env.Key}: `{env.Value}`"); } doc.AppendLine($"**Recommendation**: {drift.Recommendation}"); doc.AppendLine(); } } return doc.ToString(); } private int CalculateOverallScore(ConfigurationAnalysisResult result) { var score = 100; // Deduct points for issues score -= result.SecurityIssues.Count * 15; score -= result.ConfigurationIssues.Count * 5; score -= result.ConfigurationDrift.Count * 3; score -= result.MissingSettings.Count * 2; score -= result.DeprecatedSettings.Count * 4; return Math.Max(0, score); } } // Data models for Configuration Analysis public enum ConfigurationFileType { Json, Yaml, Xml, Properties, Unknown } public class ConfigurationFileData { public string FilePath { get; set; } public string FileName { get; set; } public ConfigurationFileType FileType { get; set; } public string Environment { get; set; } public string RawContent { get; set; } public Dictionary ParsedData { get; set; } = new(); public Dictionary FlattenedKeys { get; set; } = new(); } public class ConfigurationAnalysisResult { public string ConfigDirectory { get; set; } public int FilesAnalyzed { get; set; } public List ConfigurationDrift { get; set; } = new(); public List MissingSettings { get; set; } = new(); public List DeprecatedSettings { get; set; } = new(); public List EnvironmentIssues { get; set; } = new(); public List ConfigurationIssues { get; set; } = new(); public List SecurityIssues { get; set; } = new(); } public class ConfigurationDrift { public string Key { get; set; } public Dictionary EnvironmentValues { get; set; } = new(); public string DriftType { get; set; } public string Recommendation { get; set; } } public class MissingSetting { public string Key { get; set; } public string Environment { get; set; } public string Severity { get; set; } public string Recommendation { get; set; } } public class DeprecatedSetting { public string Key { get; set; } public string Value { get; set; } public string Location { get; set; } public string Reason { get; set; } public string Recommendation { get; set; } } public class EnvironmentIssue { public string Issue { get; set; } public string Key { get; set; } public string Value { get; set; } public string Location { get; set; } public string Recommendation { get; set; } } public class ConfigurationIssue { public string Severity { get; set; } public string Category { get; set; } public string Issue { get; set; } public string Location { get; set; } public string Recommendation { get; set; } } public class ConfigurationSecurityIssue { public string Severity { get; set; } public string Issue { get; set; } public string Location { get; set; } public string Recommendation { get; set; } public string Pattern { get; set; } } }