509 lines
20 KiB
C#
Executable File
509 lines
20 KiB
C#
Executable File
using MarketAlly.AIPlugin;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text.RegularExpressions;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace MarketAlly.AIPlugin.Security.Plugins
|
|
{
|
|
/// <summary>
|
|
/// Comprehensive security analysis including hardcoded secrets, vulnerabilities, and security patterns
|
|
/// </summary>
|
|
[AIPlugin("SecurityScan", "Comprehensive security analysis including hardcoded secrets, vulnerabilities, and security patterns")]
|
|
public class SecurityScanPlugin : IAIPlugin
|
|
{
|
|
[AIParameter("Full path to the file or directory to scan", required: true)]
|
|
public string Path { get; set; }
|
|
|
|
[AIParameter("Scan for hardcoded secrets (API keys, passwords)", required: false)]
|
|
public bool ScanSecrets { get; set; } = true;
|
|
|
|
[AIParameter("Scan for SQL injection vulnerabilities", required: false)]
|
|
public bool ScanSqlInjection { get; set; } = true;
|
|
|
|
[AIParameter("Scan for XSS vulnerabilities", required: false)]
|
|
public bool ScanXss { get; set; } = true;
|
|
|
|
[AIParameter("Scan for insecure configurations", required: false)]
|
|
public bool ScanConfigurations { get; set; } = true;
|
|
|
|
[AIParameter("Security scan severity level: low, medium, high, critical", required: false)]
|
|
public string SeverityLevel { get; set; } = "medium";
|
|
|
|
public IReadOnlyDictionary<string, Type> SupportedParameters => new Dictionary<string, Type>
|
|
{
|
|
["path"] = typeof(string),
|
|
["scanSecrets"] = typeof(bool),
|
|
["scanSqlInjection"] = typeof(bool),
|
|
["scanXss"] = typeof(bool),
|
|
["scanConfigurations"] = typeof(bool),
|
|
["severityLevel"] = typeof(string)
|
|
};
|
|
|
|
// Security patterns for detection
|
|
private static readonly Dictionary<string, (Regex Pattern, string Type, string Severity, string Description)> SecurityPatterns = new()
|
|
{
|
|
// API Keys and Secrets
|
|
["github_token"] = (new Regex(@"ghp_[a-zA-Z0-9]{36}", RegexOptions.IgnoreCase), "Secret", "High", "GitHub Personal Access Token"),
|
|
["aws_access_key"] = (new Regex(@"AKIA[0-9A-Z]{16}", RegexOptions.IgnoreCase), "Secret", "Critical", "AWS Access Key ID"),
|
|
["aws_secret_key"] = (new Regex(@"[0-9a-zA-Z/+]{40}", RegexOptions.IgnoreCase), "Secret", "Critical", "Potential AWS Secret Access Key"),
|
|
["google_api_key"] = (new Regex(@"AIza[0-9A-Za-z\-_]{35}", RegexOptions.IgnoreCase), "Secret", "High", "Google API Key"),
|
|
["slack_token"] = (new Regex(@"xox[baprs]-([0-9a-zA-Z]{10,48})", RegexOptions.IgnoreCase), "Secret", "High", "Slack Token"),
|
|
["stripe_key"] = (new Regex(@"sk_live_[0-9a-zA-Z]{24}", RegexOptions.IgnoreCase), "Secret", "Critical", "Stripe Live Secret Key"),
|
|
["twilio_api_key"] = (new Regex(@"SK[0-9a-fA-F]{32}", RegexOptions.IgnoreCase), "Secret", "High", "Twilio API Key"),
|
|
["mailgun_api_key"] = (new Regex(@"key-[0-9a-zA-Z]{32}", RegexOptions.IgnoreCase), "Secret", "High", "Mailgun API Key"),
|
|
["jwt_secret"] = (new Regex(@"(jwt|token).{0,20}['""]?\s*[A-Za-z0-9+/=]{20,}", RegexOptions.IgnoreCase), "Secret", "High", "Potential JWT Secret"),
|
|
|
|
// Generic secrets patterns
|
|
["password_hardcoded"] = (new Regex(@"(password|pwd|pass)\s*[=:]\s*['""]?\w{4,}['""]?", RegexOptions.IgnoreCase), "Secret", "High", "Hardcoded Password"),
|
|
["api_key_generic"] = (new Regex(@"(api.?key|apikey|api_key)\s*[=:]\s*['""]?\w{10,}['""]?", RegexOptions.IgnoreCase), "Secret", "High", "Hardcoded API Key"),
|
|
["secret_generic"] = (new Regex(@"(secret|token|auth)\s*[=:]\s*['""]?\w{10,}['""]?", RegexOptions.IgnoreCase), "Secret", "Medium", "Potential Hardcoded Secret"),
|
|
|
|
// SQL Injection patterns
|
|
["sql_injection_concat"] = (new Regex(@"(SELECT|INSERT|UPDATE|DELETE).*([\+]|\|\|).*['""]", RegexOptions.IgnoreCase), "SQLInjection", "High", "SQL Query String Concatenation"),
|
|
["sql_injection_format"] = (new Regex(@"String\.Format.*(SELECT|INSERT|UPDATE|DELETE)", RegexOptions.IgnoreCase), "SQLInjection", "High", "SQL Query with String.Format"),
|
|
["sql_injection_interpolation"] = (new Regex(@"\$"".*?(SELECT|INSERT|UPDATE|DELETE).*?\{.*?\}", RegexOptions.IgnoreCase), "SQLInjection", "Medium", "SQL Query with String Interpolation"),
|
|
|
|
// XSS patterns
|
|
["xss_innerhtml"] = (new Regex(@"\.innerHTML\s*=.*['""].*\+", RegexOptions.IgnoreCase), "XSS", "High", "Potential XSS via innerHTML"),
|
|
["xss_document_write"] = (new Regex(@"document\.write\(.*\+", RegexOptions.IgnoreCase), "XSS", "High", "Potential XSS via document.write"),
|
|
["xss_eval"] = (new Regex(@"eval\s*\(.*['""].*\+", RegexOptions.IgnoreCase), "XSS", "Critical", "Potential XSS via eval()"),
|
|
|
|
// Insecure configurations
|
|
["debug_enabled"] = (new Regex(@"debug\s*[=:]\s*true", RegexOptions.IgnoreCase), "Configuration", "Medium", "Debug mode enabled"),
|
|
["ssl_disabled"] = (new Regex(@"(ssl|https)\s*[=:]\s*false", RegexOptions.IgnoreCase), "Configuration", "High", "SSL/HTTPS disabled"),
|
|
["weak_encryption"] = (new Regex(@"(MD5|SHA1|DES)\s*\(", RegexOptions.IgnoreCase), "Configuration", "Medium", "Weak encryption algorithm"),
|
|
["insecure_random"] = (new Regex(@"Random\s*\(\)", RegexOptions.IgnoreCase), "Configuration", "Low", "Potentially insecure random number generation"),
|
|
|
|
// Authentication issues
|
|
["hardcoded_salt"] = (new Regex(@"salt\s*[=:]\s*['""]?\w+['""]?", RegexOptions.IgnoreCase), "Authentication", "High", "Hardcoded salt value"),
|
|
["weak_session_timeout"] = (new Regex(@"timeout\s*[=:]\s*['""]?\d{1,3}['""]?", RegexOptions.IgnoreCase), "Authentication", "Low", "Potentially weak session timeout")
|
|
};
|
|
|
|
public async Task<AIPluginResult> ExecuteAsync(IReadOnlyDictionary<string, object> parameters)
|
|
{
|
|
// Properties are auto-populated by the AIPlugin system from AI's tool call
|
|
try
|
|
{
|
|
// Validate required parameters
|
|
if (string.IsNullOrEmpty(Path))
|
|
{
|
|
return new AIPluginResult(
|
|
new ArgumentException("Path is required"),
|
|
"Path parameter is required"
|
|
);
|
|
}
|
|
|
|
string severityLevel = SeverityLevel ?? "medium";
|
|
|
|
// Validate severity level
|
|
var validSeverities = new[] { "low", "medium", "high", "critical" };
|
|
if (!validSeverities.Contains(severityLevel.ToLower()))
|
|
{
|
|
return new AIPluginResult(
|
|
new ArgumentException($"Invalid severity level: {severityLevel}. Must be one of: {string.Join(", ", validSeverities)}"),
|
|
"Invalid severity level"
|
|
);
|
|
}
|
|
|
|
// Validate path exists
|
|
if (!File.Exists(Path) && !Directory.Exists(Path))
|
|
{
|
|
return new AIPluginResult(
|
|
new FileNotFoundException($"Path not found: {Path}"),
|
|
"Path not found"
|
|
);
|
|
}
|
|
|
|
// Collect files to scan
|
|
var filesToScan = GetFilesToScan(Path);
|
|
var results = new SecurityScanResults
|
|
{
|
|
ScannedPath = Path,
|
|
FilesScanned = filesToScan.Count,
|
|
SeverityLevel = severityLevel
|
|
};
|
|
|
|
// Perform scans based on configuration
|
|
foreach (var file in filesToScan)
|
|
{
|
|
var fileContent = await File.ReadAllTextAsync(file);
|
|
var fileName = System.IO.Path.GetFileName(file);
|
|
|
|
if (ScanSecrets)
|
|
{
|
|
await ScanForSecrets(file, fileContent, results);
|
|
}
|
|
|
|
if (ScanSqlInjection)
|
|
{
|
|
await ScanForSqlInjection(file, fileContent, results);
|
|
}
|
|
|
|
if (ScanXss)
|
|
{
|
|
await ScanForXss(file, fileContent, results);
|
|
}
|
|
|
|
if (ScanConfigurations)
|
|
{
|
|
await ScanForInsecureConfigurations(file, fileContent, results);
|
|
}
|
|
}
|
|
|
|
// Filter results by severity level
|
|
FilterResultsBySeverity(results, severityLevel);
|
|
|
|
// Calculate overall risk score
|
|
results.OverallRiskScore = CalculateRiskScore(results);
|
|
results.OverallRisk = GetRiskLevel(results.OverallRiskScore);
|
|
|
|
// Generate recommendations
|
|
results.Recommendations = GenerateRecommendations(results);
|
|
|
|
return new AIPluginResult(results, $"Security scan completed. Found {results.GetTotalIssues()} security issues.");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return new AIPluginResult(ex, $"Security scan failed: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets list of files to scan based on path
|
|
/// </summary>
|
|
private List<string> GetFilesToScan(string path)
|
|
{
|
|
var files = new List<string>();
|
|
|
|
if (File.Exists(path))
|
|
{
|
|
files.Add(path);
|
|
}
|
|
else if (Directory.Exists(path))
|
|
{
|
|
// Scan common code file types
|
|
var extensions = new[] { "*.cs", "*.js", "*.ts", "*.py", "*.java", "*.php", "*.rb", "*.go", "*.cpp", "*.c", "*.h",
|
|
"*.json", "*.xml", "*.config", "*.yml", "*.yaml", "*.properties", "*.ini" };
|
|
|
|
foreach (var extension in extensions)
|
|
{
|
|
files.AddRange(Directory.GetFiles(path, extension, SearchOption.AllDirectories));
|
|
}
|
|
}
|
|
|
|
// Filter out common excluded directories
|
|
var excludedDirs = new[] { "node_modules", "bin", "obj", ".git", ".vs", "packages", "target", "build" };
|
|
files = files.Where(f => !excludedDirs.Any(dir => f.Contains($"{System.IO.Path.DirectorySeparatorChar}{dir}{System.IO.Path.DirectorySeparatorChar}"))).ToList();
|
|
|
|
return files;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Scans for hardcoded secrets and API keys
|
|
/// </summary>
|
|
private async Task ScanForSecrets(string filePath, string content, SecurityScanResults results)
|
|
{
|
|
var secretPatterns = SecurityPatterns.Where(p => p.Value.Type == "Secret").ToList();
|
|
|
|
foreach (var pattern in secretPatterns)
|
|
{
|
|
var matches = pattern.Value.Pattern.Matches(content);
|
|
foreach (Match match in matches)
|
|
{
|
|
// Skip if it looks like a comment or example
|
|
if (IsLikelyFalsePositive(content, match))
|
|
continue;
|
|
|
|
results.Secrets.Add(new SecurityIssue
|
|
{
|
|
Type = "Secret",
|
|
Severity = pattern.Value.Severity,
|
|
Description = pattern.Value.Description,
|
|
File = filePath,
|
|
LineNumber = GetLineNumber(content, match.Index),
|
|
Code = GetContextLines(content, match.Index, 1),
|
|
Recommendation = GetSecretRecommendation(pattern.Key)
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Scans for SQL injection vulnerabilities
|
|
/// </summary>
|
|
private async Task ScanForSqlInjection(string filePath, string content, SecurityScanResults results)
|
|
{
|
|
var sqlPatterns = SecurityPatterns.Where(p => p.Value.Type == "SQLInjection").ToList();
|
|
|
|
foreach (var pattern in sqlPatterns)
|
|
{
|
|
var matches = pattern.Value.Pattern.Matches(content);
|
|
foreach (Match match in matches)
|
|
{
|
|
results.Vulnerabilities.Add(new SecurityIssue
|
|
{
|
|
Type = "SQL Injection",
|
|
Severity = pattern.Value.Severity,
|
|
Description = pattern.Value.Description,
|
|
File = filePath,
|
|
LineNumber = GetLineNumber(content, match.Index),
|
|
Code = GetContextLines(content, match.Index, 2),
|
|
Recommendation = "Use parameterized queries or an ORM to prevent SQL injection. Never concatenate user input directly into SQL queries."
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Scans for XSS vulnerabilities
|
|
/// </summary>
|
|
private async Task ScanForXss(string filePath, string content, SecurityScanResults results)
|
|
{
|
|
var xssPatterns = SecurityPatterns.Where(p => p.Value.Type == "XSS").ToList();
|
|
|
|
foreach (var pattern in xssPatterns)
|
|
{
|
|
var matches = pattern.Value.Pattern.Matches(content);
|
|
foreach (Match match in matches)
|
|
{
|
|
results.Vulnerabilities.Add(new SecurityIssue
|
|
{
|
|
Type = "Cross-Site Scripting (XSS)",
|
|
Severity = pattern.Value.Severity,
|
|
Description = pattern.Value.Description,
|
|
File = filePath,
|
|
LineNumber = GetLineNumber(content, match.Index),
|
|
Code = GetContextLines(content, match.Index, 2),
|
|
Recommendation = "Sanitize and encode all user input before displaying. Use safe DOM manipulation methods and avoid eval()."
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Scans for insecure configurations
|
|
/// </summary>
|
|
private async Task ScanForInsecureConfigurations(string filePath, string content, SecurityScanResults results)
|
|
{
|
|
var configPatterns = SecurityPatterns.Where(p => p.Value.Type == "Configuration" || p.Value.Type == "Authentication").ToList();
|
|
|
|
foreach (var pattern in configPatterns)
|
|
{
|
|
var matches = pattern.Value.Pattern.Matches(content);
|
|
foreach (Match match in matches)
|
|
{
|
|
results.ConfigurationIssues.Add(new SecurityIssue
|
|
{
|
|
Type = "Configuration",
|
|
Severity = pattern.Value.Severity,
|
|
Description = pattern.Value.Description,
|
|
File = filePath,
|
|
LineNumber = GetLineNumber(content, match.Index),
|
|
Code = GetContextLines(content, match.Index, 1),
|
|
Recommendation = GetConfigurationRecommendation(pattern.Key)
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Checks if a match is likely a false positive (comment, example, etc.)
|
|
/// </summary>
|
|
private bool IsLikelyFalsePositive(string content, Match match)
|
|
{
|
|
var lineStart = content.LastIndexOf('\n', match.Index) + 1;
|
|
var lineEnd = content.IndexOf('\n', match.Index);
|
|
if (lineEnd == -1) lineEnd = content.Length;
|
|
|
|
var line = content.Substring(lineStart, lineEnd - lineStart).Trim();
|
|
|
|
// Check for comments
|
|
if (line.StartsWith("//") || line.StartsWith("#") || line.StartsWith("/*") || line.Contains("<!-- "))
|
|
return true;
|
|
|
|
// Check for common example/placeholder patterns
|
|
var falsePositivePatterns = new[] { "example", "placeholder", "your_api_key", "your_secret", "xxxxxxx", "123456", "test", "demo" };
|
|
return falsePositivePatterns.Any(pattern => match.Value.ToLower().Contains(pattern));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets the line number for a character index in content
|
|
/// </summary>
|
|
private int GetLineNumber(string content, int index)
|
|
{
|
|
return content.Take(index).Count(c => c == '\n') + 1;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets context lines around a match
|
|
/// </summary>
|
|
private string GetContextLines(string content, int index, int contextLines)
|
|
{
|
|
var lines = content.Split('\n');
|
|
var targetLine = GetLineNumber(content, index) - 1;
|
|
|
|
var startLine = Math.Max(0, targetLine - contextLines);
|
|
var endLine = Math.Min(lines.Length - 1, targetLine + contextLines);
|
|
|
|
var contextList = new List<string>();
|
|
for (int i = startLine; i <= endLine; i++)
|
|
{
|
|
var marker = i == targetLine ? ">>> " : " ";
|
|
contextList.Add($"{marker}{i + 1}: {lines[i]}");
|
|
}
|
|
|
|
return string.Join("\n", contextList);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets recommendation for a specific secret type
|
|
/// </summary>
|
|
private string GetSecretRecommendation(string secretType)
|
|
{
|
|
return secretType switch
|
|
{
|
|
"github_token" => "Move GitHub tokens to environment variables or use GitHub Secrets for CI/CD.",
|
|
"aws_access_key" or "aws_secret_key" => "Use AWS IAM roles, environment variables, or AWS Secrets Manager.",
|
|
"google_api_key" => "Store Google API keys in environment variables or Google Secret Manager.",
|
|
"stripe_key" => "Never commit Stripe keys to source control. Use environment variables and test keys for development.",
|
|
"jwt_secret" => "Use a strong, randomly generated JWT secret stored in environment variables.",
|
|
_ => "Move all secrets to environment variables, configuration files outside source control, or a secure key management system."
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets recommendation for configuration issues
|
|
/// </summary>
|
|
private string GetConfigurationRecommendation(string configType)
|
|
{
|
|
return configType switch
|
|
{
|
|
"debug_enabled" => "Disable debug mode in production environments.",
|
|
"ssl_disabled" => "Always enable SSL/HTTPS in production environments.",
|
|
"weak_encryption" => "Use strong encryption algorithms like AES-256, SHA-256, or better.",
|
|
"insecure_random" => "Use cryptographically secure random number generators for security-sensitive operations.",
|
|
"hardcoded_salt" => "Generate unique, random salts for each password hash.",
|
|
"weak_session_timeout" => "Configure appropriate session timeouts based on your security requirements.",
|
|
_ => "Review and secure this configuration according to security best practices."
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Filters results based on minimum severity level
|
|
/// </summary>
|
|
private void FilterResultsBySeverity(SecurityScanResults results, string minSeverityLevel)
|
|
{
|
|
var severityOrder = new Dictionary<string, int> { ["low"] = 1, ["medium"] = 2, ["high"] = 3, ["critical"] = 4 };
|
|
var minLevel = severityOrder[minSeverityLevel.ToLower()];
|
|
|
|
results.Secrets = results.Secrets.Where(s => severityOrder[s.Severity.ToLower()] >= minLevel).ToList();
|
|
results.Vulnerabilities = results.Vulnerabilities.Where(v => severityOrder[v.Severity.ToLower()] >= minLevel).ToList();
|
|
results.ConfigurationIssues = results.ConfigurationIssues.Where(c => severityOrder[c.Severity.ToLower()] >= minLevel).ToList();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculates overall risk score based on findings
|
|
/// </summary>
|
|
private int CalculateRiskScore(SecurityScanResults results)
|
|
{
|
|
var score = 0;
|
|
var severityWeights = new Dictionary<string, int> { ["low"] = 1, ["medium"] = 3, ["high"] = 7, ["critical"] = 15 };
|
|
|
|
foreach (var issue in results.Secrets.Concat(results.Vulnerabilities).Concat(results.ConfigurationIssues))
|
|
{
|
|
score += severityWeights[issue.Severity.ToLower()];
|
|
}
|
|
|
|
return score;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Gets risk level based on score
|
|
/// </summary>
|
|
private string GetRiskLevel(int score)
|
|
{
|
|
return score switch
|
|
{
|
|
0 => "Very Low",
|
|
<= 5 => "Low",
|
|
<= 15 => "Medium",
|
|
<= 30 => "High",
|
|
_ => "Critical"
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generates security recommendations based on findings
|
|
/// </summary>
|
|
private List<string> GenerateRecommendations(SecurityScanResults results)
|
|
{
|
|
var recommendations = new List<string>();
|
|
|
|
if (results.Secrets.Any())
|
|
{
|
|
recommendations.Add("🔑 Implement a secure secrets management strategy using environment variables, Azure Key Vault, AWS Secrets Manager, or similar services.");
|
|
recommendations.Add("📋 Audit your codebase regularly for hardcoded secrets using automated tools.");
|
|
}
|
|
|
|
if (results.Vulnerabilities.Any(v => v.Type.Contains("SQL")))
|
|
{
|
|
recommendations.Add("🛡️ Use parameterized queries, stored procedures, or ORM frameworks to prevent SQL injection attacks.");
|
|
}
|
|
|
|
if (results.Vulnerabilities.Any(v => v.Type.Contains("XSS")))
|
|
{
|
|
recommendations.Add("🧹 Implement proper input validation and output encoding to prevent XSS attacks.");
|
|
recommendations.Add("🔒 Use Content Security Policy (CSP) headers to mitigate XSS risks.");
|
|
}
|
|
|
|
if (results.ConfigurationIssues.Any())
|
|
{
|
|
recommendations.Add("⚙️ Review and harden all configuration settings for production environments.");
|
|
recommendations.Add("🔐 Use strong encryption algorithms and secure authentication mechanisms.");
|
|
}
|
|
|
|
if (results.GetTotalIssues() == 0)
|
|
{
|
|
recommendations.Add("✅ Great! No major security issues found. Continue following security best practices.");
|
|
recommendations.Add("🔄 Consider running security scans regularly as part of your CI/CD pipeline.");
|
|
}
|
|
|
|
return recommendations;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Represents the results of a security scan
|
|
/// </summary>
|
|
public class SecurityScanResults
|
|
{
|
|
public string ScannedPath { get; set; }
|
|
public int FilesScanned { get; set; }
|
|
public string SeverityLevel { get; set; }
|
|
public List<SecurityIssue> Secrets { get; set; } = new List<SecurityIssue>();
|
|
public List<SecurityIssue> Vulnerabilities { get; set; } = new List<SecurityIssue>();
|
|
public List<SecurityIssue> ConfigurationIssues { get; set; } = new List<SecurityIssue>();
|
|
public List<string> Recommendations { get; set; } = new List<string>();
|
|
public int OverallRiskScore { get; set; }
|
|
public string OverallRisk { get; set; }
|
|
|
|
public int GetTotalIssues() => Secrets.Count + Vulnerabilities.Count + ConfigurationIssues.Count;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Represents a single security issue
|
|
/// </summary>
|
|
public class SecurityIssue
|
|
{
|
|
public string Type { get; set; }
|
|
public string Severity { get; set; }
|
|
public string Description { get; set; }
|
|
public string File { get; set; }
|
|
public int LineNumber { get; set; }
|
|
public string Code { get; set; }
|
|
public string Recommendation { get; set; }
|
|
}
|
|
} |