MarketAlly.AIPlugin.Extensions/MarketAlly.AIPlugin.DevOps/DockerfileAnalyzerPlugin.cs

734 lines
24 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;
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<DockerfileAnalyzerPlugin> _logger;
public DockerfileAnalyzerPlugin(ILogger<DockerfileAnalyzerPlugin> 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<string, Type> SupportedParameters => new Dictionary<string, Type>
{
["dockerfilePath"] = typeof(string),
["checkSecurity"] = typeof(bool),
["optimizeSize"] = typeof(bool),
["checkBestPractices"] = typeof(bool),
["checkMultiStage"] = typeof(bool),
["generateOptimized"] = typeof(bool)
};
public async Task<AIPluginResult> ExecuteAsync(IReadOnlyDictionary<string, object> 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 <non-root-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<List<DockerInstruction>>();
var currentGroup = new List<DockerInstruction>();
foreach (var instruction in dockerfile.Instructions)
{
if (instruction.Command == "RUN")
{
currentGroup.Add(instruction);
}
else
{
if (currentGroup.Count > 1)
{
consecutiveRunInstructions.Add(new List<DockerInstruction>(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<int> { 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<int> { 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<DockerInstruction>(dockerfile.Instructions);
// Group consecutive RUN instructions
var newInstructions = new List<DockerInstruction>();
var runGroup = new List<DockerInstruction>();
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<DockerInstruction> Instructions { get; set; } = new List<DockerInstruction>();
}
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<DockerSecurityIssue> SecurityIssues { get; set; } = new List<DockerSecurityIssue>();
public List<DockerSizeOptimization> SizeOptimizations { get; set; } = new List<DockerSizeOptimization>();
public List<DockerBestPracticeViolation> BestPracticeViolations { get; set; } = new List<DockerBestPracticeViolation>();
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<int> LineNumbers { get; set; } = new List<int>();
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<DockerStage> Stages { get; set; } = new List<DockerStage>();
public List<string> Recommendations { get; set; } = new List<string>();
}
public class DockerStage
{
public int Index { get; set; }
public string BaseImage { get; set; }
public string Name { get; set; }
public int LineNumber { get; set; }
}
}