MarketAlly.AIPlugin.Extensions/MarketAlly.AIPlugin.DevOps/ConfigurationAnalyzerPlugin.cs

765 lines
23 KiB
C#
Executable File

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<ConfigurationAnalyzerPlugin> _logger;
private readonly IDeserializer _yamlDeserializer;
public ConfigurationAnalyzerPlugin(ILogger<ConfigurationAnalyzerPlugin> 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<string, Type> SupportedParameters => new Dictionary<string, Type>
{
["configDirectory"] = typeof(string),
["filePatterns"] = typeof(string),
["checkDrift"] = typeof(bool),
["validateEnvironments"] = typeof(bool),
["checkSettings"] = typeof(bool),
["generateDocumentation"] = typeof(bool)
};
public async Task<AIPluginResult> ExecuteAsync(IReadOnlyDictionary<string, object> 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<string, ConfigurationFileData>();
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<List<string>> DiscoverConfigurationFilesAsync(string directory, string patterns)
{
var files = new List<string>();
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<ConfigurationFileData> 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<Dictionary<string, object>>(content);
break;
case ConfigurationFileType.Yaml:
fileData.ParsedData = _yamlDeserializer.Deserialize<Dictionary<string, object>>(content);
break;
case ConfigurationFileType.Xml:
fileData.ParsedData = ParseXmlToKeyValuePairs(content);
break;
case ConfigurationFileType.Properties:
fileData.ParsedData = ParsePropertiesFile(content);
break;
default:
fileData.ParsedData = new Dictionary<string, object>();
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<string, object> ParseXmlToKeyValuePairs(string xmlContent)
{
var result = new Dictionary<string, object>();
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<string, object> result)
{
if (node == null) return;
var currentPath = string.IsNullOrEmpty(prefix) ? node.Name : $"{prefix}.{node.Name}";
if (node.HasChildNodes && node.ChildNodes.Cast<XmlNode>().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<string, object> ParsePropertiesFile(string content)
{
var result = new Dictionary<string, object>();
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<string, object> FlattenConfiguration(Dictionary<string, object> config, string prefix = "")
{
var flattened = new Dictionary<string, object>();
foreach (var kvp in config)
{
var key = string.IsNullOrEmpty(prefix) ? kvp.Key : $"{prefix}.{kvp.Key}";
if (kvp.Value is Dictionary<string, object> 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<Dictionary<string, object>>(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<string, ConfigurationFileData> 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<string, object>();
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<string, ConfigurationFileData> 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<string, ConfigurationFileData> 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<string, ConfigurationFileData> 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<string, ConfigurationFileData> 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<string, object> ParsedData { get; set; } = new();
public Dictionary<string, object> FlattenedKeys { get; set; } = new();
}
public class ConfigurationAnalysisResult
{
public string ConfigDirectory { get; set; }
public int FilesAnalyzed { get; set; }
public List<ConfigurationDrift> ConfigurationDrift { get; set; } = new();
public List<MissingSetting> MissingSettings { get; set; } = new();
public List<DeprecatedSetting> DeprecatedSettings { get; set; } = new();
public List<EnvironmentIssue> EnvironmentIssues { get; set; } = new();
public List<ConfigurationIssue> ConfigurationIssues { get; set; } = new();
public List<ConfigurationSecurityIssue> SecurityIssues { get; set; } = new();
}
public class ConfigurationDrift
{
public string Key { get; set; }
public Dictionary<string, string> 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; }
}
}