using MarketAlly.AIPlugin; using Microsoft.Extensions.Logging; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; namespace MarketAlly.AIPlugin.DevOps.Plugins { [AIPlugin("DockerfileAnalyzer", "Analyzes and optimizes Dockerfile configurations for security and performance")] public class DockerfileAnalyzerPlugin : IAIPlugin { private readonly ILogger _logger; public DockerfileAnalyzerPlugin(ILogger logger = null) { _logger = logger; } [AIParameter("Full path to the Dockerfile", required: true)] public string DockerfilePath { get; set; } [AIParameter("Check for security vulnerabilities", required: false)] public bool CheckSecurity { get; set; } = true; [AIParameter("Analyze image size optimization", required: false)] public bool OptimizeSize { get; set; } = true; [AIParameter("Check for best practices", required: false)] public bool CheckBestPractices { get; set; } = true; [AIParameter("Validate multi-stage builds", required: false)] public bool CheckMultiStage { get; set; } = true; [AIParameter("Generate optimized Dockerfile", required: false)] public bool GenerateOptimized { get; set; } = false; public IReadOnlyDictionary SupportedParameters => new Dictionary { ["dockerfilePath"] = typeof(string), ["checkSecurity"] = typeof(bool), ["optimizeSize"] = typeof(bool), ["checkBestPractices"] = typeof(bool), ["checkMultiStage"] = typeof(bool), ["generateOptimized"] = typeof(bool) }; public async Task ExecuteAsync(IReadOnlyDictionary parameters) { try { _logger?.LogInformation("DockerfileAnalyzer plugin executing"); // Extract parameters string dockerfilePath = parameters["dockerfilePath"].ToString(); bool checkSecurity = parameters.TryGetValue("checkSecurity", out var secObj) && Convert.ToBoolean(secObj); bool optimizeSize = parameters.TryGetValue("optimizeSize", out var sizeObj) && Convert.ToBoolean(sizeObj); bool checkBestPractices = parameters.TryGetValue("checkBestPractices", out var bpObj) && Convert.ToBoolean(bpObj); bool checkMultiStage = parameters.TryGetValue("checkMultiStage", out var msObj) && Convert.ToBoolean(msObj); bool generateOptimized = parameters.TryGetValue("generateOptimized", out var genObj) && Convert.ToBoolean(genObj); // Validate Dockerfile exists if (!File.Exists(dockerfilePath)) { return new AIPluginResult( new FileNotFoundException($"Dockerfile not found: {dockerfilePath}"), "Dockerfile not found" ); } // Read and parse Dockerfile var content = await File.ReadAllTextAsync(dockerfilePath); var dockerfile = ParseDockerfile(content); var analysisResult = new DockerfileAnalysisResult { FilePath = dockerfilePath, TotalInstructions = dockerfile.Instructions.Count, BaseImage = dockerfile.Instructions.FirstOrDefault(i => i.Command.Equals("FROM", StringComparison.OrdinalIgnoreCase))?.Arguments }; // Perform analysis if (checkSecurity) { AnalyzeSecurity(dockerfile, analysisResult); } if (optimizeSize) { AnalyzeSizeOptimizations(dockerfile, analysisResult); } if (checkBestPractices) { AnalyzeBestPractices(dockerfile, analysisResult); } if (checkMultiStage) { AnalyzeMultiStage(dockerfile, analysisResult); } // Generate optimized Dockerfile if requested string optimizedDockerfile = null; if (generateOptimized) { optimizedDockerfile = GenerateOptimizedDockerfile(dockerfile, analysisResult); } var result = new { Message = "Dockerfile analysis completed", DockerfilePath = dockerfilePath, BaseImage = analysisResult.BaseImage, TotalInstructions = analysisResult.TotalInstructions, SecurityIssues = analysisResult.SecurityIssues, SizeOptimizations = analysisResult.SizeOptimizations, BestPracticeViolations = analysisResult.BestPracticeViolations, MultiStageAnalysis = analysisResult.MultiStageAnalysis, OptimizedDockerfile = optimizedDockerfile, Summary = new { SecurityScore = CalculateSecurityScore(analysisResult), OptimizationScore = CalculateOptimizationScore(analysisResult), BestPracticeScore = CalculateBestPracticeScore(analysisResult), OverallScore = CalculateOverallScore(analysisResult) } }; _logger?.LogInformation("Dockerfile analysis completed. Found {SecurityIssues} security issues, {SizeOptimizations} size optimizations, {BestPractices} best practice violations", analysisResult.SecurityIssues.Count, analysisResult.SizeOptimizations.Count, analysisResult.BestPracticeViolations.Count); return new AIPluginResult(result); } catch (Exception ex) { _logger?.LogError(ex, "Failed to analyze Dockerfile"); return new AIPluginResult(ex, "Failed to analyze Dockerfile"); } } private DockerfileStructure ParseDockerfile(string content) { var dockerfile = new DockerfileStructure(); var lines = content.Split('\n', StringSplitOptions.RemoveEmptyEntries); for (int i = 0; i < lines.Length; i++) { var line = lines[i].Trim(); // Skip comments and empty lines if (string.IsNullOrWhiteSpace(line) || line.StartsWith("#")) continue; // Handle line continuations while (line.EndsWith("\\") && i + 1 < lines.Length) { line = line.Substring(0, line.Length - 1) + " " + lines[++i].Trim(); } var parts = line.Split(new[] { ' ', '\t' }, 2, StringSplitOptions.RemoveEmptyEntries); if (parts.Length >= 1) { var instruction = new DockerInstruction { Command = parts[0].ToUpper(), Arguments = parts.Length > 1 ? parts[1] : string.Empty, LineNumber = i + 1, OriginalLine = line }; dockerfile.Instructions.Add(instruction); } } return dockerfile; } private void AnalyzeSecurity(DockerfileStructure dockerfile, DockerfileAnalysisResult result) { // Check for running as root var userInstructions = dockerfile.Instructions.Where(i => i.Command == "USER").ToList(); if (!userInstructions.Any()) { result.SecurityIssues.Add(new DockerSecurityIssue { Severity = "High", Issue = "Container runs as root user", LineNumber = null, Recommendation = "Add 'USER ' instruction to run container with limited privileges", Description = "Running containers as root increases security risk" }); } else { // Check if any USER instruction uses root foreach (var userInstruction in userInstructions) { if (userInstruction.Arguments.Contains("root") || userInstruction.Arguments.Contains("0")) { result.SecurityIssues.Add(new DockerSecurityIssue { Severity = "High", Issue = "Explicitly setting user to root", LineNumber = userInstruction.LineNumber, Recommendation = "Use a non-root user instead", Description = "Explicitly running as root is a security risk" }); } } } // Check for hardcoded secrets var secretPatterns = new[] { @"(?i)(password|pwd|pass|secret|token|key|api[-_]?key)[\s]*[=:][\s]*[""']?[a-zA-Z0-9+/]{8,}[""']?", @"(?i)AWS[-_]?(ACCESS[-_]?KEY[-_]?ID|SECRET[-_]?ACCESS[-_]?KEY)", @"(?i)(github|gitlab)[-_]?token", @"(?i)docker[-_]?password" }; foreach (var instruction in dockerfile.Instructions) { foreach (var pattern in secretPatterns) { var matches = Regex.Matches(instruction.Arguments, pattern); foreach (Match match in matches) { result.SecurityIssues.Add(new DockerSecurityIssue { Severity = "Critical", Issue = "Potential hardcoded secret detected", LineNumber = instruction.LineNumber, Recommendation = "Use Docker secrets, environment variables, or build-time secrets instead", Description = $"Found potential secret: {match.Value.Substring(0, Math.Min(20, match.Value.Length))}..." }); } } } // Check for ADD instead of COPY for local files foreach (var instruction in dockerfile.Instructions.Where(i => i.Command == "ADD")) { if (!instruction.Arguments.StartsWith("http://") && !instruction.Arguments.StartsWith("https://")) { result.SecurityIssues.Add(new DockerSecurityIssue { Severity = "Medium", Issue = "Using ADD for local files", LineNumber = instruction.LineNumber, Recommendation = "Use COPY instead of ADD for local files", Description = "ADD has additional functionality that can introduce security risks" }); } } // Check for latest tag usage foreach (var instruction in dockerfile.Instructions.Where(i => i.Command == "FROM")) { if (instruction.Arguments.EndsWith(":latest") || !instruction.Arguments.Contains(":")) { result.SecurityIssues.Add(new DockerSecurityIssue { Severity = "Medium", Issue = "Using latest or unspecified tag", LineNumber = instruction.LineNumber, Recommendation = "Pin to specific version tags for reproducible and secure builds", Description = "Latest tags can introduce unexpected changes and security vulnerabilities" }); } } // Check for package manager cache not being cleaned var runInstructions = dockerfile.Instructions.Where(i => i.Command == "RUN").ToList(); foreach (var runInstruction in runInstructions) { var args = runInstruction.Arguments.ToLower(); if ((args.Contains("apt-get install") && !args.Contains("rm -rf /var/lib/apt/lists/*")) || (args.Contains("yum install") && !args.Contains("yum clean all")) || (args.Contains("apk add") && !args.Contains("rm -rf /var/cache/apk/*"))) { result.SecurityIssues.Add(new DockerSecurityIssue { Severity = "Low", Issue = "Package manager cache not cleaned", LineNumber = runInstruction.LineNumber, Recommendation = "Clean package manager cache to reduce image size and attack surface", Description = "Leftover package manager cache increases image size unnecessarily" }); } } } private void AnalyzeSizeOptimizations(DockerfileStructure dockerfile, DockerfileAnalysisResult result) { // Check for layer consolidation opportunities var consecutiveRunInstructions = new List>(); var currentGroup = new List(); foreach (var instruction in dockerfile.Instructions) { if (instruction.Command == "RUN") { currentGroup.Add(instruction); } else { if (currentGroup.Count > 1) { consecutiveRunInstructions.Add(new List(currentGroup)); } currentGroup.Clear(); } } if (currentGroup.Count > 1) { consecutiveRunInstructions.Add(currentGroup); } foreach (var group in consecutiveRunInstructions) { result.SizeOptimizations.Add(new DockerSizeOptimization { Type = "Layer Consolidation", Description = $"Found {group.Count} consecutive RUN instructions that could be combined", LineNumbers = group.Select(i => i.LineNumber).ToList(), Recommendation = "Combine consecutive RUN instructions using && to reduce layer count", EstimatedSizeSaving = "10-30% reduction in image layers" }); } // Check for .dockerignore file var dockerignorePath = Path.Combine(Path.GetDirectoryName(result.FilePath), ".dockerignore"); if (!File.Exists(dockerignorePath)) { result.SizeOptimizations.Add(new DockerSizeOptimization { Type = "Build Context", Description = "No .dockerignore file found", Recommendation = "Create a .dockerignore file to exclude unnecessary files from build context", EstimatedSizeSaving = "Significant reduction in build time and image size" }); } // Check for unnecessary packages var runInstructions = dockerfile.Instructions.Where(i => i.Command == "RUN").ToList(); foreach (var runInstruction in runInstructions) { var args = runInstruction.Arguments.ToLower(); // Check for development tools in production images var devTools = new[] { "gcc", "g++", "make", "cmake", "git", "wget", "curl" }; foreach (var tool in devTools) { if (args.Contains($"install.*{tool}") || args.Contains($"add.*{tool}")) { result.SizeOptimizations.Add(new DockerSizeOptimization { Type = "Development Tools", Description = $"Development tool '{tool}' being installed", LineNumbers = new List { runInstruction.LineNumber }, Recommendation = "Consider using multi-stage builds to exclude development tools from final image", EstimatedSizeSaving = "20-50% reduction in image size" }); } } } // Check for Alpine Linux usage for smaller base images var fromInstructions = dockerfile.Instructions.Where(i => i.Command == "FROM").ToList(); foreach (var fromInstruction in fromInstructions) { if (!fromInstruction.Arguments.Contains("alpine") && !fromInstruction.Arguments.Contains("slim")) { result.SizeOptimizations.Add(new DockerSizeOptimization { Type = "Base Image", Description = "Using full-size base image", LineNumbers = new List { fromInstruction.LineNumber }, Recommendation = "Consider using Alpine Linux or slim variants for smaller image size", EstimatedSizeSaving = "50-80% reduction in base image size" }); } } } private void AnalyzeBestPractices(DockerfileStructure dockerfile, DockerfileAnalysisResult result) { // Check for LABEL instructions if (!dockerfile.Instructions.Any(i => i.Command == "LABEL")) { result.BestPracticeViolations.Add(new DockerBestPracticeViolation { Rule = "Image Metadata", Description = "No LABEL instructions found", Recommendation = "Add LABEL instructions for maintainer, version, and description", Impact = "Poor image discoverability and maintenance" }); } // Check for HEALTHCHECK if (!dockerfile.Instructions.Any(i => i.Command == "HEALTHCHECK")) { result.BestPracticeViolations.Add(new DockerBestPracticeViolation { Rule = "Health Monitoring", Description = "No HEALTHCHECK instruction found", Recommendation = "Add HEALTHCHECK instruction to monitor container health", Impact = "No automatic health monitoring capability" }); } // Check for EXPOSE instruction if (!dockerfile.Instructions.Any(i => i.Command == "EXPOSE")) { result.BestPracticeViolations.Add(new DockerBestPracticeViolation { Rule = "Port Documentation", Description = "No EXPOSE instruction found", Recommendation = "Add EXPOSE instruction to document which ports the container listens on", Impact = "Poor documentation of network requirements" }); } // Check for proper ordering of instructions var instructionOrder = dockerfile.Instructions.Select(i => i.Command).ToList(); var idealOrder = new[] { "FROM", "LABEL", "ARG", "ENV", "RUN", "COPY", "ADD", "EXPOSE", "USER", "WORKDIR", "CMD", "ENTRYPOINT" }; for (int i = 1; i < instructionOrder.Count; i++) { var current = instructionOrder[i]; var previous = instructionOrder[i - 1]; var currentIndex = Array.IndexOf(idealOrder, current); var previousIndex = Array.IndexOf(idealOrder, previous); if (currentIndex != -1 && previousIndex != -1 && currentIndex < previousIndex) { result.BestPracticeViolations.Add(new DockerBestPracticeViolation { Rule = "Instruction Ordering", Description = $"Instruction {current} should typically come before {previous}", LineNumber = dockerfile.Instructions[i].LineNumber, Recommendation = "Reorder instructions to follow Docker best practices", Impact = "Suboptimal layer caching and build performance" }); } } // Check for absolute paths in WORKDIR foreach (var instruction in dockerfile.Instructions.Where(i => i.Command == "WORKDIR")) { if (!instruction.Arguments.StartsWith("/")) { result.BestPracticeViolations.Add(new DockerBestPracticeViolation { Rule = "Absolute Paths", Description = "WORKDIR should use absolute paths", LineNumber = instruction.LineNumber, Recommendation = "Use absolute paths in WORKDIR instructions", Impact = "Potential path resolution issues" }); } } } private void AnalyzeMultiStage(DockerfileStructure dockerfile, DockerfileAnalysisResult result) { var fromInstructions = dockerfile.Instructions.Where(i => i.Command == "FROM").ToList(); var isMultiStage = fromInstructions.Count > 1; result.MultiStageAnalysis = new DockerMultiStageAnalysis { IsMultiStage = isMultiStage, StageCount = fromInstructions.Count, Stages = fromInstructions.Select((instr, index) => new DockerStage { Index = index, BaseImage = instr.Arguments.Split(' ')[0], Name = instr.Arguments.Contains(" AS ") ? instr.Arguments.Split(" AS ")[1].Trim() : null, LineNumber = instr.LineNumber }).ToList() }; if (!isMultiStage) { // Check if the dockerfile could benefit from multi-stage builds var hasCompileSteps = dockerfile.Instructions.Any(i => i.Command == "RUN" && (i.Arguments.Contains("compile") || i.Arguments.Contains("build") || i.Arguments.Contains("npm install") || i.Arguments.Contains("dotnet build") || i.Arguments.Contains("mvn compile"))); if (hasCompileSteps) { result.MultiStageAnalysis.Recommendations.Add( "Consider using multi-stage builds to separate build dependencies from runtime image" ); } } else { // Analyze multi-stage efficiency var finalStage = result.MultiStageAnalysis.Stages.Last(); if (string.IsNullOrEmpty(finalStage.Name)) { result.MultiStageAnalysis.Recommendations.Add( "Consider naming your final stage for better readability" ); } // Check for proper COPY --from usage var copyFromInstructions = dockerfile.Instructions .Where(i => i.Command == "COPY" && i.Arguments.Contains("--from=")) .ToList(); if (copyFromInstructions.Count == 0) { result.MultiStageAnalysis.Recommendations.Add( "Multi-stage build detected but no COPY --from instructions found. Ensure you're copying artifacts between stages." ); } } } private string GenerateOptimizedDockerfile(DockerfileStructure dockerfile, DockerfileAnalysisResult result) { var optimized = new StringBuilder(); optimized.AppendLine("# Optimized Dockerfile generated by MarketAlly.AIPlugin.DevOps"); optimized.AppendLine("# Original file: " + result.FilePath); optimized.AppendLine(); // Apply optimizations based on analysis var instructions = new List(dockerfile.Instructions); // Group consecutive RUN instructions var newInstructions = new List(); var runGroup = new List(); foreach (var instruction in instructions) { if (instruction.Command == "RUN") { runGroup.Add(instruction); } else { if (runGroup.Count > 1) { // Combine RUN instructions var combinedArgs = string.Join(" && \\\n ", runGroup.Select(r => r.Arguments)); newInstructions.Add(new DockerInstruction { Command = "RUN", Arguments = combinedArgs, LineNumber = runGroup.First().LineNumber, OriginalLine = $"RUN {combinedArgs}" }); } else if (runGroup.Count == 1) { newInstructions.Add(runGroup[0]); } runGroup.Clear(); newInstructions.Add(instruction); } } // Handle remaining RUN group if (runGroup.Count > 1) { var combinedArgs = string.Join(" && \\\n ", runGroup.Select(r => r.Arguments)); newInstructions.Add(new DockerInstruction { Command = "RUN", Arguments = combinedArgs, LineNumber = runGroup.First().LineNumber, OriginalLine = $"RUN {combinedArgs}" }); } else if (runGroup.Count == 1) { newInstructions.Add(runGroup[0]); } // Add missing best practice instructions if (!newInstructions.Any(i => i.Command == "LABEL")) { optimized.AppendLine("LABEL maintainer=\"your-email@domain.com\""); optimized.AppendLine("LABEL version=\"1.0\""); optimized.AppendLine("LABEL description=\"Application container\""); optimized.AppendLine(); } // Output optimized instructions foreach (var instruction in newInstructions) { optimized.AppendLine($"{instruction.Command} {instruction.Arguments}"); } // Add missing instructions based on analysis if (!newInstructions.Any(i => i.Command == "USER")) { optimized.AppendLine(); optimized.AppendLine("# Add non-root user for security"); optimized.AppendLine("RUN addgroup -g 1001 -S appgroup && \\"); optimized.AppendLine(" adduser -u 1001 -S appuser -G appgroup"); optimized.AppendLine("USER appuser"); } if (!newInstructions.Any(i => i.Command == "HEALTHCHECK")) { optimized.AppendLine(); optimized.AppendLine("# Add health check (customize as needed)"); optimized.AppendLine("HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \\"); optimized.AppendLine(" CMD curl -f http://localhost:8080/health || exit 1"); } return optimized.ToString(); } private int CalculateSecurityScore(DockerfileAnalysisResult result) { var score = 100; foreach (var issue in result.SecurityIssues) { score -= issue.Severity switch { "Critical" => 25, "High" => 15, "Medium" => 10, "Low" => 5, _ => 5 }; } return Math.Max(0, score); } private int CalculateOptimizationScore(DockerfileAnalysisResult result) { var score = 100 - (result.SizeOptimizations.Count * 10); return Math.Max(0, score); } private int CalculateBestPracticeScore(DockerfileAnalysisResult result) { var score = 100 - (result.BestPracticeViolations.Count * 8); return Math.Max(0, score); } private int CalculateOverallScore(DockerfileAnalysisResult result) { var securityScore = CalculateSecurityScore(result); var optimizationScore = CalculateOptimizationScore(result); var bestPracticeScore = CalculateBestPracticeScore(result); // Weighted average: Security 40%, Optimization 30%, Best Practices 30% return (int)(securityScore * 0.4 + optimizationScore * 0.3 + bestPracticeScore * 0.3); } } // Data models for Dockerfile analysis public class DockerfileStructure { public List Instructions { get; set; } = new List(); } public class DockerInstruction { public string Command { get; set; } public string Arguments { get; set; } public int LineNumber { get; set; } public string OriginalLine { get; set; } } public class DockerfileAnalysisResult { public string FilePath { get; set; } public string BaseImage { get; set; } public int TotalInstructions { get; set; } public List SecurityIssues { get; set; } = new List(); public List SizeOptimizations { get; set; } = new List(); public List BestPracticeViolations { get; set; } = new List(); public DockerMultiStageAnalysis MultiStageAnalysis { get; set; } } public class DockerSecurityIssue { public string Severity { get; set; } public string Issue { get; set; } public int? LineNumber { get; set; } public string Recommendation { get; set; } public string Description { get; set; } } public class DockerSizeOptimization { public string Type { get; set; } public string Description { get; set; } public List LineNumbers { get; set; } = new List(); public string Recommendation { get; set; } public string EstimatedSizeSaving { get; set; } } public class DockerBestPracticeViolation { public string Rule { get; set; } public string Description { get; set; } public int? LineNumber { get; set; } public string Recommendation { get; set; } public string Impact { get; set; } } public class DockerMultiStageAnalysis { public bool IsMultiStage { get; set; } public int StageCount { get; set; } public List Stages { get; set; } = new List(); public List Recommendations { get; set; } = new List(); } public class DockerStage { public int Index { get; set; } public string BaseImage { get; set; } public string Name { get; set; } public int LineNumber { get; set; } } }