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 _logger; private readonly IDeserializer _yamlDeserializer; public DevOpsScanPlugin(ILogger 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 SupportedParameters => new Dictionary { ["pipelinePath"] = typeof(string), ["pipelineType"] = typeof(string), ["checkSecurity"] = typeof(bool), ["optimizeBuild"] = typeof(bool), ["checkBestPractices"] = typeof(bool), ["generateRecommendations"] = typeof(bool) }; public async Task ExecuteAsync(IReadOnlyDictionary 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(); // 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(); // 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> DiscoverPipelineFilesAsync(string path) { var pipelineFiles = new List(); 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 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(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 { ["build"] = new GitHubJob { Name = "build", RunsOn = "ubuntu-latest", Steps = new List { 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(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(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 GenerateRecommendationsMethod(List results) { var recommendations = new List(); 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 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 On { get; set; } public Dictionary Permissions { get; set; } public Dictionary 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 Needs { get; set; } public GitHubStrategy Strategy { get; set; } public List Steps { get; set; } } public class GitHubStrategy { public Dictionary Matrix { get; set; } } public class GitHubStep { public string Name { get; set; } public string Uses { get; set; } public string Run { get; set; } public Dictionary With { get; set; } } // Result models public class PipelineAnalysisResult { public string FilePath { get; set; } public string PipelineType { get; set; } public List SecurityIssues { get; set; } = new List(); public List OptimizationOpportunities { get; set; } = new List(); public List BestPracticeViolations { get; set; } = new List(); } 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 Variables { get; set; } public List Stages { get; set; } public List Jobs { get; set; } // For single-stage pipelines } public class AzurePool { public string VmImage { get; set; } public string Name { get; set; } public Dictionary Demands { get; set; } } public class AzureStage { public string Stage { get; set; } public string DisplayName { get; set; } public string Condition { get; set; } public List DependsOn { get; set; } public List Jobs { get; set; } } public class AzureJob { public string Job { get; set; } public string DisplayName { get; set; } public AzurePool Pool { get; set; } public List DependsOn { get; set; } public string Condition { get; set; } public List Steps { get; set; } public AzureStrategy Strategy { get; set; } } public class AzureStep { public string Task { get; set; } public string DisplayName { get; set; } public Dictionary Inputs { get; set; } public string Condition { get; set; } public string Script { get; set; } } public class AzureStrategy { public Dictionary Matrix { get; set; } public int? MaxParallel { get; set; } } // GitLab CI data models public class GitLabCIPipeline { public List Stages { get; set; } public Dictionary Variables { get; set; } public GitLabCache Cache { get; set; } public string Image { get; set; } public List Services { get; set; } public List BeforeScript { get; set; } public List AfterScript { get; set; } public Dictionary Jobs { get; set; } } public class GitLabJob { public string Name { get; set; } public string Stage { get; set; } public string Image { get; set; } public List Services { get; set; } public List BeforeScript { get; set; } public List Script { get; set; } public List AfterScript { get; set; } public GitLabArtifacts Artifacts { get; set; } public GitLabCache Cache { get; set; } public Dictionary Variables { get; set; } public List Only { get; set; } public List Except { get; set; } public List 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 Dependencies { get; set; } public List Needs { get; set; } } public class GitLabArtifacts { public List 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 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; } } }