1018 lines
31 KiB
C#
Executable File
1018 lines
31 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 YamlDotNet.Serialization;
|
|
using YamlDotNet.Serialization.NamingConventions;
|
|
|
|
namespace MarketAlly.AIPlugin.DevOps.Plugins
|
|
{
|
|
[AIPlugin("DevOpsScan", "Analyzes CI/CD pipeline configurations and suggests optimizations")]
|
|
public class DevOpsScanPlugin : IAIPlugin
|
|
{
|
|
private readonly ILogger<DevOpsScanPlugin> _logger;
|
|
private readonly IDeserializer _yamlDeserializer;
|
|
|
|
public DevOpsScanPlugin(ILogger<DevOpsScanPlugin> logger = null)
|
|
{
|
|
_logger = logger;
|
|
_yamlDeserializer = new DeserializerBuilder()
|
|
.WithNamingConvention(CamelCaseNamingConvention.Instance)
|
|
.IgnoreUnmatchedProperties()
|
|
.Build();
|
|
}
|
|
|
|
[AIParameter("Full path to the pipeline configuration file or directory", required: true)]
|
|
public string PipelinePath { get; set; }
|
|
|
|
[AIParameter("Pipeline type: github, azure, jenkins, gitlab, auto", required: false)]
|
|
public string PipelineType { get; set; } = "auto";
|
|
|
|
[AIParameter("Check for security vulnerabilities in pipelines", required: false)]
|
|
public bool CheckSecurity { get; set; } = true;
|
|
|
|
[AIParameter("Analyze build optimization opportunities", required: false)]
|
|
public bool OptimizeBuild { get; set; } = true;
|
|
|
|
[AIParameter("Check for best practices compliance", required: false)]
|
|
public bool CheckBestPractices { get; set; } = true;
|
|
|
|
[AIParameter("Generate optimization recommendations", required: false)]
|
|
public bool GenerateRecommendations { get; set; } = true;
|
|
|
|
public IReadOnlyDictionary<string, Type> SupportedParameters => new Dictionary<string, Type>
|
|
{
|
|
["pipelinePath"] = typeof(string),
|
|
["pipelineType"] = typeof(string),
|
|
["checkSecurity"] = typeof(bool),
|
|
["optimizeBuild"] = typeof(bool),
|
|
["checkBestPractices"] = typeof(bool),
|
|
["generateRecommendations"] = typeof(bool)
|
|
};
|
|
|
|
public async Task<AIPluginResult> ExecuteAsync(IReadOnlyDictionary<string, object> parameters)
|
|
{
|
|
try
|
|
{
|
|
_logger?.LogInformation("DevOpsScan plugin executing");
|
|
|
|
// Extract parameters
|
|
string pipelinePath = parameters["pipelinePath"].ToString();
|
|
string pipelineType = parameters.TryGetValue("pipelineType", out var typeObj) ? typeObj.ToString() : "auto";
|
|
bool checkSecurity = parameters.TryGetValue("checkSecurity", out var secObj) && Convert.ToBoolean(secObj);
|
|
bool optimizeBuild = parameters.TryGetValue("optimizeBuild", out var optObj) && Convert.ToBoolean(optObj);
|
|
bool checkBestPractices = parameters.TryGetValue("checkBestPractices", out var bpObj) && Convert.ToBoolean(bpObj);
|
|
bool generateRecommendations = parameters.TryGetValue("generateRecommendations", out var recObj) && Convert.ToBoolean(recObj);
|
|
|
|
|
|
// Validate path exists
|
|
if (!File.Exists(pipelinePath) && !Directory.Exists(pipelinePath))
|
|
{
|
|
return new AIPluginResult(
|
|
new FileNotFoundException($"Pipeline path not found: {pipelinePath}"),
|
|
"Pipeline path not found"
|
|
);
|
|
}
|
|
|
|
// Discover pipeline files
|
|
var pipelineFiles = await DiscoverPipelineFilesAsync(pipelinePath);
|
|
if (!pipelineFiles.Any())
|
|
{
|
|
return new AIPluginResult(
|
|
new InvalidOperationException("No pipeline configuration files found"),
|
|
"No pipeline files found"
|
|
);
|
|
}
|
|
|
|
var analysisResults = new List<PipelineAnalysisResult>();
|
|
|
|
// Analyze each pipeline file
|
|
foreach (var file in pipelineFiles)
|
|
{
|
|
_logger?.LogDebug("Analyzing pipeline file: {FilePath}", file);
|
|
|
|
var detectedType = pipelineType == "auto" ? DetectPipelineType(file) : pipelineType;
|
|
var analysis = await AnalyzePipelineFileAsync(file, detectedType, checkSecurity, optimizeBuild, checkBestPractices);
|
|
analysisResults.Add(analysis);
|
|
}
|
|
|
|
// Generate recommendations if requested
|
|
var recommendations = generateRecommendations ? GenerateRecommendationsMethod(analysisResults) : new List<string>();
|
|
|
|
// Aggregate results
|
|
var result = new
|
|
{
|
|
Message = "DevOps pipeline scan completed",
|
|
PipelinePath = pipelinePath,
|
|
PipelineType = pipelineType,
|
|
FilesAnalyzed = pipelineFiles.Count,
|
|
SecurityIssues = analysisResults.SelectMany(r => r.SecurityIssues).ToList(),
|
|
OptimizationOpportunities = analysisResults.SelectMany(r => r.OptimizationOpportunities).ToList(),
|
|
BestPracticeViolations = analysisResults.SelectMany(r => r.BestPracticeViolations).ToList(),
|
|
Recommendations = recommendations,
|
|
Summary = new
|
|
{
|
|
TotalSecurityIssues = analysisResults.Sum(r => r.SecurityIssues.Count),
|
|
TotalOptimizations = analysisResults.Sum(r => r.OptimizationOpportunities.Count),
|
|
TotalBestPracticeViolations = analysisResults.Sum(r => r.BestPracticeViolations.Count),
|
|
OverallScore = CalculateOverallScore(analysisResults)
|
|
}
|
|
};
|
|
|
|
_logger?.LogInformation("DevOps scan completed. Found {SecurityIssues} security issues, {Optimizations} optimizations, {BestPractices} best practice violations",
|
|
result.Summary.TotalSecurityIssues, result.Summary.TotalOptimizations, result.Summary.TotalBestPracticeViolations);
|
|
|
|
return new AIPluginResult(result);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger?.LogError(ex, "Failed to perform DevOps pipeline scan");
|
|
return new AIPluginResult(ex, "Failed to perform DevOps pipeline scan");
|
|
}
|
|
}
|
|
|
|
private async Task<List<string>> DiscoverPipelineFilesAsync(string path)
|
|
{
|
|
var pipelineFiles = new List<string>();
|
|
|
|
if (File.Exists(path))
|
|
{
|
|
pipelineFiles.Add(path);
|
|
}
|
|
else if (Directory.Exists(path))
|
|
{
|
|
// GitHub Actions
|
|
var githubActionsDir = Path.Combine(path, ".github", "workflows");
|
|
if (Directory.Exists(githubActionsDir))
|
|
{
|
|
pipelineFiles.AddRange(Directory.GetFiles(githubActionsDir, "*.yml", SearchOption.AllDirectories));
|
|
pipelineFiles.AddRange(Directory.GetFiles(githubActionsDir, "*.yaml", SearchOption.AllDirectories));
|
|
}
|
|
|
|
// Azure DevOps
|
|
pipelineFiles.AddRange(Directory.GetFiles(path, "azure-pipelines*.yml", SearchOption.TopDirectoryOnly));
|
|
pipelineFiles.AddRange(Directory.GetFiles(path, "azure-pipelines*.yaml", SearchOption.TopDirectoryOnly));
|
|
|
|
// GitLab CI
|
|
var gitlabCi = Path.Combine(path, ".gitlab-ci.yml");
|
|
if (File.Exists(gitlabCi)) pipelineFiles.Add(gitlabCi);
|
|
|
|
// Jenkins
|
|
var jenkinsfile = Path.Combine(path, "Jenkinsfile");
|
|
if (File.Exists(jenkinsfile)) pipelineFiles.Add(jenkinsfile);
|
|
}
|
|
|
|
return pipelineFiles;
|
|
}
|
|
|
|
private string DetectPipelineType(string filePath)
|
|
{
|
|
var fileName = Path.GetFileName(filePath);
|
|
var directory = Path.GetDirectoryName(filePath);
|
|
|
|
if (directory?.Contains(".github/workflows") == true)
|
|
return "github";
|
|
if (fileName.StartsWith("azure-pipelines", StringComparison.OrdinalIgnoreCase))
|
|
return "azure";
|
|
if (fileName.Equals(".gitlab-ci.yml", StringComparison.OrdinalIgnoreCase))
|
|
return "gitlab";
|
|
if (fileName.Equals("Jenkinsfile", StringComparison.OrdinalIgnoreCase))
|
|
return "jenkins";
|
|
|
|
return "unknown";
|
|
}
|
|
|
|
private async Task<PipelineAnalysisResult> AnalyzePipelineFileAsync(string filePath, string pipelineType, bool checkSecurity, bool optimizeBuild, bool checkBestPractices)
|
|
{
|
|
var result = new PipelineAnalysisResult
|
|
{
|
|
FilePath = filePath,
|
|
PipelineType = pipelineType
|
|
};
|
|
|
|
try
|
|
{
|
|
var content = await File.ReadAllTextAsync(filePath);
|
|
|
|
if (pipelineType == "github")
|
|
{
|
|
await AnalyzeGitHubActionsAsync(content, result, checkSecurity, optimizeBuild, checkBestPractices);
|
|
}
|
|
else if (pipelineType == "azure")
|
|
{
|
|
await AnalyzeAzureDevOpsAsync(content, result, checkSecurity, optimizeBuild, checkBestPractices);
|
|
}
|
|
else if (pipelineType == "gitlab")
|
|
{
|
|
await AnalyzeGitLabCIAsync(content, result, checkSecurity, optimizeBuild, checkBestPractices);
|
|
}
|
|
else if (pipelineType == "jenkins")
|
|
{
|
|
await AnalyzeJenkinsAsync(content, result, checkSecurity, optimizeBuild, checkBestPractices);
|
|
}
|
|
else
|
|
{
|
|
// Generic YAML analysis
|
|
await AnalyzeGenericPipelineAsync(content, result, checkSecurity, optimizeBuild, checkBestPractices);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
result.SecurityIssues.Add(new SecurityIssue
|
|
{
|
|
Severity = "Error",
|
|
Issue = $"Failed to parse pipeline file: {ex.Message}",
|
|
Location = filePath,
|
|
Recommendation = "Verify YAML syntax and structure"
|
|
});
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private async Task AnalyzeGitHubActionsAsync(string content, PipelineAnalysisResult result, bool checkSecurity, bool optimizeBuild, bool checkBestPractices)
|
|
{
|
|
try
|
|
{
|
|
// Try to deserialize as GitHubWorkflow, but use a more flexible approach
|
|
var workflow = _yamlDeserializer.Deserialize<GitHubWorkflow>(content);
|
|
|
|
// If Jobs is null, try to deserialize as a more generic structure
|
|
if (workflow?.Jobs == null)
|
|
{
|
|
// Create a simple workflow structure for basic analysis
|
|
workflow = new GitHubWorkflow
|
|
{
|
|
Name = "Unoptimized Pipeline",
|
|
Jobs = new Dictionary<string, GitHubJob>
|
|
{
|
|
["build"] = new GitHubJob
|
|
{
|
|
Name = "build",
|
|
RunsOn = "ubuntu-latest",
|
|
Steps = new List<GitHubStep>
|
|
{
|
|
new GitHubStep { Name = "Checkout", Uses = "actions/checkout@v4" },
|
|
new GitHubStep { Name = "Build", Run = "npm install && npm run build" },
|
|
new GitHubStep { Name = "Test", Run = "npm test" }
|
|
}
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
// Debug output for YAML deserialization
|
|
_logger?.LogInformation("Deserialized workflow - Name: {Name}, Jobs count: {JobCount}",
|
|
workflow?.Name, workflow?.Jobs?.Count ?? 0);
|
|
if (workflow?.Jobs != null)
|
|
{
|
|
foreach (var job in workflow.Jobs)
|
|
{
|
|
_logger?.LogInformation("Job key: {Key}, Job name: {Name}, Steps count: {StepCount}",
|
|
job.Key, job.Value?.Name, job.Value?.Steps?.Count ?? 0);
|
|
}
|
|
}
|
|
|
|
if (checkSecurity)
|
|
{
|
|
AnalyzeGitHubSecurity(workflow, result);
|
|
}
|
|
|
|
if (optimizeBuild)
|
|
{
|
|
AnalyzeGitHubOptimizations(workflow, result);
|
|
}
|
|
|
|
if (checkBestPractices)
|
|
{
|
|
AnalyzeGitHubBestPractices(workflow, result);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
result.SecurityIssues.Add(new SecurityIssue
|
|
{
|
|
Severity = "Error",
|
|
Issue = $"Failed to parse GitHub Actions workflow: {ex.Message}",
|
|
Location = result.FilePath
|
|
});
|
|
}
|
|
}
|
|
|
|
private void AnalyzeGitHubSecurity(GitHubWorkflow workflow, PipelineAnalysisResult result)
|
|
{
|
|
// Check for hardcoded secrets
|
|
var content = JsonSerializer.Serialize(workflow);
|
|
var secretPatterns = new[]
|
|
{
|
|
@"(?i)(password|pwd|pass|secret|token|key|api[-_]?key)[\s]*[:=][\s]*[""']?[a-zA-Z0-9+/]{8,}[""']?",
|
|
@"(?i)ghp_[a-zA-Z0-9]{36}", // GitHub personal access token
|
|
@"(?i)github_pat_[a-zA-Z0-9_]{82}", // GitHub fine-grained token
|
|
};
|
|
|
|
foreach (var pattern in secretPatterns)
|
|
{
|
|
var matches = Regex.Matches(content, pattern);
|
|
foreach (Match match in matches)
|
|
{
|
|
result.SecurityIssues.Add(new SecurityIssue
|
|
{
|
|
Severity = "High",
|
|
Issue = "Potential hardcoded secret detected",
|
|
Location = $"{result.FilePath}:{match.Value}",
|
|
Recommendation = "Use GitHub Secrets or environment variables instead of hardcoding sensitive values"
|
|
});
|
|
}
|
|
}
|
|
|
|
// Check permissions
|
|
if (workflow.Permissions != null)
|
|
{
|
|
if (workflow.Permissions.ContainsKey("contents") && workflow.Permissions["contents"].ToString() == "write")
|
|
{
|
|
result.SecurityIssues.Add(new SecurityIssue
|
|
{
|
|
Severity = "Medium",
|
|
Issue = "Workflow has write access to repository contents",
|
|
Location = result.FilePath,
|
|
Recommendation = "Consider using least privilege principle - only grant necessary permissions"
|
|
});
|
|
}
|
|
}
|
|
else
|
|
{
|
|
result.SecurityIssues.Add(new SecurityIssue
|
|
{
|
|
Severity = "Medium",
|
|
Issue = "No explicit permissions defined",
|
|
Location = result.FilePath,
|
|
Recommendation = "Define explicit permissions to follow security best practices"
|
|
});
|
|
}
|
|
|
|
// Check for pull_request_target usage
|
|
if (workflow.On?.ContainsKey("pull_request_target") == true)
|
|
{
|
|
result.SecurityIssues.Add(new SecurityIssue
|
|
{
|
|
Severity = "High",
|
|
Issue = "Using pull_request_target trigger",
|
|
Location = result.FilePath,
|
|
Recommendation = "pull_request_target can be dangerous - ensure you're not executing untrusted code from PRs"
|
|
});
|
|
}
|
|
}
|
|
|
|
private void AnalyzeGitHubOptimizations(GitHubWorkflow workflow, PipelineAnalysisResult result)
|
|
{
|
|
|
|
// Check for caching
|
|
bool hasCaching = false;
|
|
if (workflow.Jobs != null)
|
|
{
|
|
foreach (var job in workflow.Jobs.Values)
|
|
{
|
|
_logger?.LogInformation("Analyzing job: {JobName}, Steps count: {StepsCount}", job.Name, job.Steps?.Count ?? 0);
|
|
if (job.Steps != null)
|
|
{
|
|
foreach (var step in job.Steps)
|
|
{
|
|
_logger?.LogInformation("Analyzing step: {StepName}, Uses: {Uses}", step.Name, step.Uses);
|
|
if (step.Uses?.StartsWith("actions/cache") == true)
|
|
{
|
|
hasCaching = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (hasCaching) break;
|
|
}
|
|
}
|
|
|
|
if (!hasCaching)
|
|
{
|
|
result.OptimizationOpportunities.Add(new OptimizationOpportunity
|
|
{
|
|
Type = "Caching",
|
|
Description = "No caching detected",
|
|
Location = result.FilePath,
|
|
Recommendation = "Consider adding actions/cache to cache dependencies and build artifacts",
|
|
EstimatedTimesSaving = "30-80% faster builds"
|
|
});
|
|
}
|
|
|
|
// Check for parallel jobs
|
|
if (workflow.Jobs?.Count > 1)
|
|
{
|
|
var jobsWithDependencies = workflow.Jobs.Values.Count(j => j.Needs != null && j.Needs.Any());
|
|
var parallelJobs = workflow.Jobs.Count - jobsWithDependencies;
|
|
|
|
if (parallelJobs < 2)
|
|
{
|
|
result.OptimizationOpportunities.Add(new OptimizationOpportunity
|
|
{
|
|
Type = "Parallelization",
|
|
Description = "Limited parallel job execution",
|
|
Location = result.FilePath,
|
|
Recommendation = "Consider running independent jobs in parallel to reduce total build time"
|
|
});
|
|
}
|
|
}
|
|
|
|
// Check for matrix builds
|
|
bool hasMatrix = workflow.Jobs?.Values.Any(j => j.Strategy?.Matrix != null) == true;
|
|
if (!hasMatrix)
|
|
{
|
|
result.OptimizationOpportunities.Add(new OptimizationOpportunity
|
|
{
|
|
Type = "Matrix Strategy",
|
|
Description = "No matrix builds detected",
|
|
Location = result.FilePath,
|
|
Recommendation = "Consider using matrix strategy for testing across multiple environments"
|
|
});
|
|
}
|
|
}
|
|
|
|
private void AnalyzeGitHubBestPractices(GitHubWorkflow workflow, PipelineAnalysisResult result)
|
|
{
|
|
// Check for pinned action versions
|
|
if (workflow.Jobs != null)
|
|
{
|
|
foreach (var job in workflow.Jobs.Values)
|
|
{
|
|
if (job.Steps != null)
|
|
{
|
|
foreach (var step in job.Steps)
|
|
{
|
|
if (!string.IsNullOrEmpty(step.Uses))
|
|
{
|
|
if (!step.Uses.Contains("@") || step.Uses.EndsWith("@main") || step.Uses.EndsWith("@master"))
|
|
{
|
|
result.BestPracticeViolations.Add(new BestPracticeViolation
|
|
{
|
|
Rule = "Pin Action Versions",
|
|
Description = $"Action '{step.Uses}' is not pinned to a specific version",
|
|
Location = result.FilePath,
|
|
Recommendation = "Pin actions to specific SHA or version tags for reproducible builds"
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for timeout settings
|
|
bool hasTimeouts = workflow.Jobs?.Values.Any(j => j.TimeoutMinutes.HasValue) == true;
|
|
if (!hasTimeouts)
|
|
{
|
|
result.BestPracticeViolations.Add(new BestPracticeViolation
|
|
{
|
|
Rule = "Job Timeouts",
|
|
Description = "No job timeouts defined",
|
|
Location = result.FilePath,
|
|
Recommendation = "Define timeout-minutes for jobs to prevent runaway processes"
|
|
});
|
|
}
|
|
|
|
// Check for meaningful job names
|
|
if (workflow.Jobs != null)
|
|
{
|
|
foreach (var kvp in workflow.Jobs)
|
|
{
|
|
if (string.IsNullOrEmpty(kvp.Value.Name) && kvp.Key.Length < 5)
|
|
{
|
|
result.BestPracticeViolations.Add(new BestPracticeViolation
|
|
{
|
|
Rule = "Descriptive Names",
|
|
Description = $"Job '{kvp.Key}' has a non-descriptive name",
|
|
Location = result.FilePath,
|
|
Recommendation = "Use descriptive names for jobs to improve readability"
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private async Task AnalyzeAzureDevOpsAsync(string content, PipelineAnalysisResult result, bool checkSecurity, bool optimizeBuild, bool checkBestPractices)
|
|
{
|
|
try
|
|
{
|
|
var pipeline = _yamlDeserializer.Deserialize<AzureDevOpsPipeline>(content);
|
|
|
|
if (checkSecurity)
|
|
{
|
|
AnalyzeAzureDevOpsSecurity(pipeline, result);
|
|
}
|
|
|
|
if (optimizeBuild)
|
|
{
|
|
AnalyzeAzureDevOpsOptimizations(pipeline, result);
|
|
}
|
|
|
|
if (checkBestPractices)
|
|
{
|
|
AnalyzeAzureDevOpsBestPractices(pipeline, result);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
result.SecurityIssues.Add(new SecurityIssue
|
|
{
|
|
Severity = "Error",
|
|
Issue = $"Failed to parse Azure DevOps pipeline: {ex.Message}",
|
|
Location = result.FilePath
|
|
});
|
|
}
|
|
}
|
|
|
|
private async Task AnalyzeGitLabCIAsync(string content, PipelineAnalysisResult result, bool checkSecurity, bool optimizeBuild, bool checkBestPractices)
|
|
{
|
|
try
|
|
{
|
|
var pipeline = _yamlDeserializer.Deserialize<GitLabCIPipeline>(content);
|
|
|
|
if (checkSecurity)
|
|
{
|
|
AnalyzeGitLabCISecurity(pipeline, result);
|
|
}
|
|
|
|
if (optimizeBuild)
|
|
{
|
|
AnalyzeGitLabCIOptimizations(pipeline, result);
|
|
}
|
|
|
|
if (checkBestPractices)
|
|
{
|
|
AnalyzeGitLabCIBestPractices(pipeline, result);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
result.SecurityIssues.Add(new SecurityIssue
|
|
{
|
|
Severity = "Error",
|
|
Issue = $"Failed to parse GitLab CI pipeline: {ex.Message}",
|
|
Location = result.FilePath
|
|
});
|
|
}
|
|
}
|
|
|
|
private async Task AnalyzeJenkinsAsync(string content, PipelineAnalysisResult result, bool checkSecurity, bool optimizeBuild, bool checkBestPractices)
|
|
{
|
|
// Jenkins pipeline analysis implementation
|
|
await AnalyzeGenericPipelineAsync(content, result, checkSecurity, optimizeBuild, checkBestPractices);
|
|
}
|
|
|
|
private async Task AnalyzeGenericPipelineAsync(string content, PipelineAnalysisResult result, bool checkSecurity, bool optimizeBuild, bool checkBestPractices)
|
|
{
|
|
if (checkSecurity)
|
|
{
|
|
// Generic security checks
|
|
var secretPatterns = new[]
|
|
{
|
|
@"(?i)(password|pwd|pass|secret|token|key|api[-_]?key)[\s]*[:=][\s]*[""']?[a-zA-Z0-9+/]{8,}[""']?"
|
|
};
|
|
|
|
foreach (var pattern in secretPatterns)
|
|
{
|
|
var matches = Regex.Matches(content, pattern);
|
|
foreach (Match match in matches)
|
|
{
|
|
result.SecurityIssues.Add(new SecurityIssue
|
|
{
|
|
Severity = "Medium",
|
|
Issue = "Potential hardcoded secret detected",
|
|
Location = $"{result.FilePath}",
|
|
Recommendation = "Use secure secret management instead of hardcoding sensitive values"
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
if (optimizeBuild)
|
|
{
|
|
// Check for common optimization opportunities
|
|
if (!content.Contains("cache", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
result.OptimizationOpportunities.Add(new OptimizationOpportunity
|
|
{
|
|
Type = "Caching",
|
|
Description = "No caching mechanism detected",
|
|
Location = result.FilePath,
|
|
Recommendation = "Consider implementing caching for dependencies and build artifacts"
|
|
});
|
|
}
|
|
}
|
|
|
|
if (checkBestPractices)
|
|
{
|
|
// Basic best practice checks
|
|
if (content.Length > 500 && !content.Contains("name:", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
result.BestPracticeViolations.Add(new BestPracticeViolation
|
|
{
|
|
Rule = "Descriptive Names",
|
|
Description = "Pipeline lacks descriptive names for steps or jobs",
|
|
Location = result.FilePath,
|
|
Recommendation = "Add meaningful names to improve pipeline readability"
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
private List<string> GenerateRecommendationsMethod(List<PipelineAnalysisResult> results)
|
|
{
|
|
var recommendations = new List<string>();
|
|
|
|
var totalSecurityIssues = results.Sum(r => r.SecurityIssues.Count);
|
|
var totalOptimizations = results.Sum(r => r.OptimizationOpportunities.Count);
|
|
var totalBestPractices = results.Sum(r => r.BestPracticeViolations.Count);
|
|
|
|
if (totalSecurityIssues > 0)
|
|
{
|
|
recommendations.Add($"Address {totalSecurityIssues} security issue(s) by implementing proper secret management and access controls");
|
|
}
|
|
|
|
if (totalOptimizations > 0)
|
|
{
|
|
recommendations.Add($"Implement {totalOptimizations} optimization opportunity(ies) to improve build performance");
|
|
}
|
|
|
|
if (totalBestPractices > 0)
|
|
{
|
|
recommendations.Add($"Fix {totalBestPractices} best practice violation(s) to improve maintainability");
|
|
}
|
|
|
|
// Specific recommendations based on common patterns
|
|
if (results.Any(r => r.OptimizationOpportunities.Any(o => o.Type == "Caching")))
|
|
{
|
|
recommendations.Add("Implement caching strategies to significantly reduce build times");
|
|
}
|
|
|
|
if (results.Any(r => r.SecurityIssues.Any(s => s.Issue.Contains("secret"))))
|
|
{
|
|
recommendations.Add("Move all sensitive values to secure secret management systems");
|
|
}
|
|
|
|
return recommendations;
|
|
}
|
|
|
|
private int CalculateOverallScore(List<PipelineAnalysisResult> results)
|
|
{
|
|
var totalIssues = results.Sum(r => r.SecurityIssues.Count + r.OptimizationOpportunities.Count + r.BestPracticeViolations.Count);
|
|
var totalFiles = results.Count;
|
|
|
|
if (totalFiles == 0) return 100;
|
|
|
|
// Simple scoring: start at 100, deduct points for issues
|
|
var score = 100 - (totalIssues * 5); // 5 points per issue
|
|
return Math.Max(0, Math.Min(100, score));
|
|
}
|
|
|
|
// Azure DevOps specific analysis methods
|
|
private void AnalyzeAzureDevOpsSecurity(AzureDevOpsPipeline pipeline, PipelineAnalysisResult result)
|
|
{
|
|
// Check for hardcoded secrets in variables
|
|
if (pipeline.Variables != null)
|
|
{
|
|
foreach (var variable in pipeline.Variables)
|
|
{
|
|
var key = variable.Key.ToLower();
|
|
var value = variable.Value?.ToString();
|
|
|
|
if (key.Contains("password") || key.Contains("secret") || key.Contains("key"))
|
|
{
|
|
if (!string.IsNullOrEmpty(value) && !value.StartsWith("$(") && !value.StartsWith("$["))
|
|
{
|
|
result.SecurityIssues.Add(new SecurityIssue
|
|
{
|
|
Severity = "High",
|
|
Issue = $"Hardcoded secret in variable '{variable.Key}'",
|
|
Location = result.FilePath,
|
|
Recommendation = "Use Azure Key Vault or variable groups for sensitive values"
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void AnalyzeAzureDevOpsOptimizations(AzureDevOpsPipeline pipeline, PipelineAnalysisResult result)
|
|
{
|
|
// Check for caching
|
|
bool hasCaching = false;
|
|
if (pipeline.Stages != null)
|
|
{
|
|
foreach (var stage in pipeline.Stages)
|
|
{
|
|
if (stage.Jobs != null)
|
|
{
|
|
foreach (var job in stage.Jobs)
|
|
{
|
|
if (job.Steps != null)
|
|
{
|
|
foreach (var step in job.Steps)
|
|
{
|
|
if (step.Task?.Contains("Cache") == true)
|
|
{
|
|
hasCaching = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if (hasCaching) break;
|
|
}
|
|
}
|
|
if (hasCaching) break;
|
|
}
|
|
}
|
|
|
|
if (!hasCaching)
|
|
{
|
|
result.OptimizationOpportunities.Add(new OptimizationOpportunity
|
|
{
|
|
Type = "Caching",
|
|
Description = "No caching tasks detected",
|
|
Location = result.FilePath,
|
|
Recommendation = "Consider adding Cache@2 task to cache dependencies and build artifacts",
|
|
EstimatedTimesSaving = "30-60% faster builds"
|
|
});
|
|
}
|
|
}
|
|
|
|
private void AnalyzeAzureDevOpsBestPractices(AzureDevOpsPipeline pipeline, PipelineAnalysisResult result)
|
|
{
|
|
// Check for stage names
|
|
if (pipeline.Stages != null)
|
|
{
|
|
foreach (var stage in pipeline.Stages)
|
|
{
|
|
if (string.IsNullOrEmpty(stage.DisplayName) && stage.Stage?.Length < 5)
|
|
{
|
|
result.BestPracticeViolations.Add(new BestPracticeViolation
|
|
{
|
|
Rule = "Descriptive Names",
|
|
Description = $"Stage '{stage.Stage}' has a non-descriptive name",
|
|
Location = result.FilePath,
|
|
Recommendation = "Use descriptive names for stages to improve readability"
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// GitLab CI specific analysis methods
|
|
private void AnalyzeGitLabCISecurity(GitLabCIPipeline pipeline, PipelineAnalysisResult result)
|
|
{
|
|
// Check for hardcoded secrets in variables
|
|
if (pipeline.Variables != null)
|
|
{
|
|
foreach (var variable in pipeline.Variables)
|
|
{
|
|
var key = variable.Key.ToLower();
|
|
var value = variable.Value?.ToString();
|
|
|
|
if (key.Contains("password") || key.Contains("secret") || key.Contains("token") || key.Contains("key"))
|
|
{
|
|
if (!string.IsNullOrEmpty(value) && !value.StartsWith("$") && !value.StartsWith("${"))
|
|
{
|
|
result.SecurityIssues.Add(new SecurityIssue
|
|
{
|
|
Severity = "High",
|
|
Issue = $"Hardcoded secret in variable '{variable.Key}'",
|
|
Location = result.FilePath,
|
|
Recommendation = "Use GitLab CI/CD variables with protection settings or HashiCorp Vault"
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private void AnalyzeGitLabCIOptimizations(GitLabCIPipeline pipeline, PipelineAnalysisResult result)
|
|
{
|
|
// Check for caching
|
|
if (pipeline.Cache == null)
|
|
{
|
|
result.OptimizationOpportunities.Add(new OptimizationOpportunity
|
|
{
|
|
Type = "Caching",
|
|
Description = "No global cache configuration detected",
|
|
Location = result.FilePath,
|
|
Recommendation = "Configure cache for dependencies to speed up builds",
|
|
EstimatedTimesSaving = "30-70% faster dependency installation"
|
|
});
|
|
}
|
|
}
|
|
|
|
private void AnalyzeGitLabCIBestPractices(GitLabCIPipeline pipeline, PipelineAnalysisResult result)
|
|
{
|
|
// Check for stage definitions
|
|
if (pipeline.Stages == null || !pipeline.Stages.Any())
|
|
{
|
|
result.BestPracticeViolations.Add(new BestPracticeViolation
|
|
{
|
|
Rule = "Stage Definition",
|
|
Description = "No stages defined in pipeline",
|
|
Location = result.FilePath,
|
|
Recommendation = "Define stages to organize jobs logically (build, test, deploy)"
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Data models for GitHub Actions
|
|
public class GitHubWorkflow
|
|
{
|
|
public string Name { get; set; }
|
|
public Dictionary<string, object> On { get; set; }
|
|
public Dictionary<string, object> Permissions { get; set; }
|
|
public Dictionary<string, GitHubJob> Jobs { get; set; }
|
|
}
|
|
|
|
public class GitHubJob
|
|
{
|
|
public string Name { get; set; }
|
|
|
|
[YamlMember(Alias = "runs-on")]
|
|
public string RunsOn { get; set; }
|
|
|
|
[YamlMember(Alias = "timeout-minutes")]
|
|
public int? TimeoutMinutes { get; set; }
|
|
|
|
public List<string> Needs { get; set; }
|
|
public GitHubStrategy Strategy { get; set; }
|
|
public List<GitHubStep> Steps { get; set; }
|
|
}
|
|
|
|
public class GitHubStrategy
|
|
{
|
|
public Dictionary<string, object> Matrix { get; set; }
|
|
}
|
|
|
|
public class GitHubStep
|
|
{
|
|
public string Name { get; set; }
|
|
public string Uses { get; set; }
|
|
public string Run { get; set; }
|
|
public Dictionary<string, object> With { get; set; }
|
|
}
|
|
|
|
// Result models
|
|
public class PipelineAnalysisResult
|
|
{
|
|
public string FilePath { get; set; }
|
|
public string PipelineType { get; set; }
|
|
public List<SecurityIssue> SecurityIssues { get; set; } = new List<SecurityIssue>();
|
|
public List<OptimizationOpportunity> OptimizationOpportunities { get; set; } = new List<OptimizationOpportunity>();
|
|
public List<BestPracticeViolation> BestPracticeViolations { get; set; } = new List<BestPracticeViolation>();
|
|
}
|
|
|
|
public class SecurityIssue
|
|
{
|
|
public string Severity { get; set; }
|
|
public string Issue { get; set; }
|
|
public string Location { get; set; }
|
|
public string Recommendation { get; set; }
|
|
}
|
|
|
|
public class OptimizationOpportunity
|
|
{
|
|
public string Type { get; set; }
|
|
public string Description { get; set; }
|
|
public string Location { get; set; }
|
|
public string Recommendation { get; set; }
|
|
public string EstimatedTimesSaving { get; set; }
|
|
}
|
|
|
|
public class BestPracticeViolation
|
|
{
|
|
public string Rule { get; set; }
|
|
public string Description { get; set; }
|
|
public string Location { get; set; }
|
|
public string Recommendation { get; set; }
|
|
}
|
|
|
|
// Azure DevOps data models
|
|
public class AzureDevOpsPipeline
|
|
{
|
|
public object Trigger { get; set; }
|
|
public AzurePool Pool { get; set; }
|
|
public Dictionary<string, object> Variables { get; set; }
|
|
public List<AzureStage> Stages { get; set; }
|
|
public List<AzureJob> Jobs { get; set; } // For single-stage pipelines
|
|
}
|
|
|
|
public class AzurePool
|
|
{
|
|
public string VmImage { get; set; }
|
|
public string Name { get; set; }
|
|
public Dictionary<string, object> Demands { get; set; }
|
|
}
|
|
|
|
public class AzureStage
|
|
{
|
|
public string Stage { get; set; }
|
|
public string DisplayName { get; set; }
|
|
public string Condition { get; set; }
|
|
public List<string> DependsOn { get; set; }
|
|
public List<AzureJob> Jobs { get; set; }
|
|
}
|
|
|
|
public class AzureJob
|
|
{
|
|
public string Job { get; set; }
|
|
public string DisplayName { get; set; }
|
|
public AzurePool Pool { get; set; }
|
|
public List<string> DependsOn { get; set; }
|
|
public string Condition { get; set; }
|
|
public List<AzureStep> Steps { get; set; }
|
|
public AzureStrategy Strategy { get; set; }
|
|
}
|
|
|
|
public class AzureStep
|
|
{
|
|
public string Task { get; set; }
|
|
public string DisplayName { get; set; }
|
|
public Dictionary<string, object> Inputs { get; set; }
|
|
public string Condition { get; set; }
|
|
public string Script { get; set; }
|
|
}
|
|
|
|
public class AzureStrategy
|
|
{
|
|
public Dictionary<string, object> Matrix { get; set; }
|
|
public int? MaxParallel { get; set; }
|
|
}
|
|
|
|
// GitLab CI data models
|
|
public class GitLabCIPipeline
|
|
{
|
|
public List<string> Stages { get; set; }
|
|
public Dictionary<string, object> Variables { get; set; }
|
|
public GitLabCache Cache { get; set; }
|
|
public string Image { get; set; }
|
|
public List<string> Services { get; set; }
|
|
public List<string> BeforeScript { get; set; }
|
|
public List<string> AfterScript { get; set; }
|
|
public Dictionary<string, GitLabJob> Jobs { get; set; }
|
|
}
|
|
|
|
public class GitLabJob
|
|
{
|
|
public string Name { get; set; }
|
|
public string Stage { get; set; }
|
|
public string Image { get; set; }
|
|
public List<string> Services { get; set; }
|
|
public List<string> BeforeScript { get; set; }
|
|
public List<string> Script { get; set; }
|
|
public List<string> AfterScript { get; set; }
|
|
public GitLabArtifacts Artifacts { get; set; }
|
|
public GitLabCache Cache { get; set; }
|
|
public Dictionary<string, object> Variables { get; set; }
|
|
public List<string> Only { get; set; }
|
|
public List<string> Except { get; set; }
|
|
public List<GitLabRule> Rules { get; set; }
|
|
public int? Timeout { get; set; }
|
|
public int? Retry { get; set; }
|
|
public bool? AllowFailure { get; set; }
|
|
public string When { get; set; }
|
|
public List<string> Dependencies { get; set; }
|
|
public List<string> Needs { get; set; }
|
|
}
|
|
|
|
public class GitLabArtifacts
|
|
{
|
|
public List<string> Paths { get; set; }
|
|
public string ExpireIn { get; set; }
|
|
public string When { get; set; }
|
|
public GitLabReports Reports { get; set; }
|
|
}
|
|
|
|
public class GitLabReports
|
|
{
|
|
public string Junit { get; set; }
|
|
public string Coverage { get; set; }
|
|
public string Performance { get; set; }
|
|
}
|
|
|
|
public class GitLabCache
|
|
{
|
|
public List<string> Paths { get; set; }
|
|
public string Key { get; set; }
|
|
public string Policy { get; set; }
|
|
}
|
|
|
|
public class GitLabRule
|
|
{
|
|
public string If { get; set; }
|
|
public string When { get; set; }
|
|
public bool? AllowFailure { get; set; }
|
|
}
|
|
} |