765 lines
23 KiB
C#
Executable File
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; }
|
|
}
|
|
} |