MarketAlly.AIPlugin.Extensions/MarketAlly.AIPlugin.DevOps/DevOpsScanPlugin.cs

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; }
}
}