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