partly or unimplemented features - now implemented
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Text.RegularExpressions;
|
||||
using StellaOps.Integrations.Contracts.AiCodeGuard;
|
||||
|
||||
namespace StellaOps.Integrations.WebService.AiCodeGuard;
|
||||
|
||||
public interface IAiCodeGuardPipelineConfigLoader
|
||||
{
|
||||
AiCodeGuardRunConfiguration Load(string? yaml);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Minimal deterministic YAML loader for AI Code Guard pipeline settings.
|
||||
/// </summary>
|
||||
public sealed class AiCodeGuardPipelineConfigLoader : IAiCodeGuardPipelineConfigLoader
|
||||
{
|
||||
public AiCodeGuardRunConfiguration Load(string? yaml)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(yaml))
|
||||
{
|
||||
return new AiCodeGuardRunConfiguration();
|
||||
}
|
||||
|
||||
var enableSecrets = true;
|
||||
var enableAttribution = true;
|
||||
var enableLicense = true;
|
||||
var maxFindings = 200;
|
||||
|
||||
var allowedLicenses = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var customPatterns = new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
string? activeList = null;
|
||||
foreach (var rawLine in EnumerateLines(yaml))
|
||||
{
|
||||
var line = rawLine.Trim();
|
||||
if (line.Length == 0 || line.StartsWith('#'))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.StartsWith("-", StringComparison.Ordinal))
|
||||
{
|
||||
if (activeList is null)
|
||||
{
|
||||
throw new FormatException($"List entry is not attached to a key: '{line}'.");
|
||||
}
|
||||
|
||||
var item = line[1..].Trim();
|
||||
if (string.IsNullOrWhiteSpace(item))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (activeList)
|
||||
{
|
||||
case "allowedspdxlicenses":
|
||||
allowedLicenses.Add(item);
|
||||
break;
|
||||
case "customsecretpatterns":
|
||||
ValidateRegexPattern(item);
|
||||
customPatterns.Add(item);
|
||||
break;
|
||||
default:
|
||||
throw new FormatException($"Unsupported list key '{activeList}'.");
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
var separatorIndex = line.IndexOf(':');
|
||||
if (separatorIndex < 0)
|
||||
{
|
||||
throw new FormatException($"Invalid YAML entry '{line}'. Expected key:value.");
|
||||
}
|
||||
|
||||
var key = NormalizeKey(line[..separatorIndex]);
|
||||
var value = line[(separatorIndex + 1)..].Trim();
|
||||
|
||||
if (value.Length == 0)
|
||||
{
|
||||
activeList = key switch
|
||||
{
|
||||
"allowedspdxlicenses" or "licenseallowlist" => "allowedspdxlicenses",
|
||||
"customsecretpatterns" or "secretpatterns" => "customsecretpatterns",
|
||||
_ => throw new FormatException($"Unsupported list key '{key}'."),
|
||||
};
|
||||
continue;
|
||||
}
|
||||
|
||||
activeList = null;
|
||||
switch (key)
|
||||
{
|
||||
case "enablesecretsscan":
|
||||
case "secrets":
|
||||
enableSecrets = ParseBoolean(value, key);
|
||||
break;
|
||||
case "enableattributioncheck":
|
||||
case "attribution":
|
||||
enableAttribution = ParseBoolean(value, key);
|
||||
break;
|
||||
case "enablelicensehygiene":
|
||||
case "license":
|
||||
enableLicense = ParseBoolean(value, key);
|
||||
break;
|
||||
case "maxfindings":
|
||||
maxFindings = ParseMaxFindings(value);
|
||||
break;
|
||||
case "allowedspdxlicenses":
|
||||
case "licenseallowlist":
|
||||
foreach (var item in ParseInlineList(value))
|
||||
{
|
||||
allowedLicenses.Add(item);
|
||||
}
|
||||
break;
|
||||
case "customsecretpatterns":
|
||||
case "secretpatterns":
|
||||
foreach (var pattern in ParseInlineList(value))
|
||||
{
|
||||
ValidateRegexPattern(pattern);
|
||||
customPatterns.Add(pattern);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new FormatException($"Unsupported configuration key '{key}'.");
|
||||
}
|
||||
}
|
||||
|
||||
return new AiCodeGuardRunConfiguration
|
||||
{
|
||||
EnableSecretsScan = enableSecrets,
|
||||
EnableAttributionCheck = enableAttribution,
|
||||
EnableLicenseHygiene = enableLicense,
|
||||
MaxFindings = maxFindings,
|
||||
AllowedSpdxLicenses = allowedLicenses
|
||||
.OrderBy(static value => value, StringComparer.Ordinal)
|
||||
.ToImmutableArray(),
|
||||
CustomSecretPatterns = customPatterns
|
||||
.OrderBy(static value => value, StringComparer.Ordinal)
|
||||
.ToImmutableArray(),
|
||||
};
|
||||
}
|
||||
|
||||
private static IEnumerable<string> EnumerateLines(string yaml)
|
||||
{
|
||||
return yaml
|
||||
.Replace("\r\n", "\n", StringComparison.Ordinal)
|
||||
.Split('\n');
|
||||
}
|
||||
|
||||
private static string NormalizeKey(string key)
|
||||
{
|
||||
return key.Trim().ToLowerInvariant().Replace("_", string.Empty, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static bool ParseBoolean(string value, string key)
|
||||
{
|
||||
return value.Trim().ToLowerInvariant() switch
|
||||
{
|
||||
"true" or "yes" or "on" => true,
|
||||
"false" or "no" or "off" => false,
|
||||
_ => throw new FormatException($"Invalid boolean value '{value}' for key '{key}'."),
|
||||
};
|
||||
}
|
||||
|
||||
private static int ParseMaxFindings(string value)
|
||||
{
|
||||
if (!int.TryParse(value, out var parsed) || parsed < 1)
|
||||
{
|
||||
throw new FormatException($"Invalid maxFindings value '{value}'. Expected positive integer.");
|
||||
}
|
||||
|
||||
return parsed;
|
||||
}
|
||||
|
||||
private static IEnumerable<string> ParseInlineList(string value)
|
||||
{
|
||||
var normalized = value.Trim();
|
||||
if (normalized.StartsWith('[') && normalized.EndsWith(']'))
|
||||
{
|
||||
normalized = normalized[1..^1];
|
||||
}
|
||||
|
||||
return normalized
|
||||
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Where(static entry => !string.IsNullOrWhiteSpace(entry))
|
||||
.Select(static entry => entry.Trim('"', '\''));
|
||||
}
|
||||
|
||||
private static void ValidateRegexPattern(string pattern)
|
||||
{
|
||||
_ = new Regex(pattern, RegexOptions.CultureInvariant);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,396 @@
|
||||
using System.Collections.Immutable;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using StellaOps.Integrations.Contracts.AiCodeGuard;
|
||||
|
||||
namespace StellaOps.Integrations.WebService.AiCodeGuard;
|
||||
|
||||
public interface IAiCodeGuardRunService
|
||||
{
|
||||
Task<AiCodeGuardRunResponse> RunAsync(
|
||||
AiCodeGuardRunRequest request,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deterministic standalone AI Code Guard runner for secrets, attribution, and license hygiene.
|
||||
/// </summary>
|
||||
public sealed class AiCodeGuardRunService : IAiCodeGuardRunService
|
||||
{
|
||||
private static readonly Regex SpdxLicenseRegex = new(
|
||||
@"SPDX-License-Identifier:\s*(?<id>[A-Za-z0-9\.\-\+]+)",
|
||||
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
|
||||
|
||||
private static readonly ImmutableArray<SecretRule> BuiltInSecretRules =
|
||||
[
|
||||
new(
|
||||
"AICG-SECRET-AWS-ACCESS-KEY",
|
||||
"Secrets",
|
||||
"Potential AWS access key detected.",
|
||||
new Regex(@"AKIA[0-9A-Z]{16}", RegexOptions.CultureInvariant),
|
||||
AnnotationLevel.Failure,
|
||||
0.98),
|
||||
new(
|
||||
"AICG-SECRET-GITHUB-TOKEN",
|
||||
"Secrets",
|
||||
"Potential GitHub personal access token detected.",
|
||||
new Regex(@"ghp_[A-Za-z0-9]{36}", RegexOptions.CultureInvariant),
|
||||
AnnotationLevel.Failure,
|
||||
0.95),
|
||||
new(
|
||||
"AICG-SECRET-PRIVATE-KEY",
|
||||
"Secrets",
|
||||
"Private key material detected.",
|
||||
new Regex(@"-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----", RegexOptions.CultureInvariant),
|
||||
AnnotationLevel.Failure,
|
||||
0.99),
|
||||
];
|
||||
|
||||
private static readonly ImmutableArray<string> AttributionMarkers =
|
||||
[
|
||||
"generated by chatgpt",
|
||||
"generated with chatgpt",
|
||||
"copilot",
|
||||
"ai-generated",
|
||||
"generated by ai",
|
||||
];
|
||||
|
||||
private readonly IAiCodeGuardPipelineConfigLoader _configLoader;
|
||||
private readonly ILogger<AiCodeGuardRunService> _logger;
|
||||
|
||||
public AiCodeGuardRunService(
|
||||
IAiCodeGuardPipelineConfigLoader configLoader,
|
||||
ILogger<AiCodeGuardRunService> logger)
|
||||
{
|
||||
_configLoader = configLoader ?? throw new ArgumentNullException(nameof(configLoader));
|
||||
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
}
|
||||
|
||||
public Task<AiCodeGuardRunResponse> RunAsync(
|
||||
AiCodeGuardRunRequest request,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
var configuration = _configLoader.Load(request.ConfigYaml);
|
||||
var files = request.Files
|
||||
.OrderBy(static file => NormalizePath(file.Path), StringComparer.Ordinal)
|
||||
.ToArray();
|
||||
|
||||
var findings = new List<AiCodeGuardFindingAnnotation>();
|
||||
var filesWithFindings = new HashSet<string>(StringComparer.Ordinal);
|
||||
var attributionFiles = new HashSet<string>(StringComparer.Ordinal);
|
||||
|
||||
var secretRules = BuildSecretRules(configuration);
|
||||
foreach (var file in files)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var path = NormalizePath(file.Path);
|
||||
var lines = GetLines(file.Content);
|
||||
var findingsBefore = findings.Count;
|
||||
|
||||
if (configuration.EnableSecretsScan)
|
||||
{
|
||||
ScanSecrets(path, lines, secretRules, findings);
|
||||
}
|
||||
|
||||
if (configuration.EnableAttributionCheck)
|
||||
{
|
||||
ScanAttribution(path, lines, findings, attributionFiles);
|
||||
}
|
||||
|
||||
if (configuration.EnableLicenseHygiene)
|
||||
{
|
||||
ScanLicense(path, lines, configuration.AllowedSpdxLicenses, findings);
|
||||
}
|
||||
|
||||
if (findings.Count > findingsBefore)
|
||||
{
|
||||
filesWithFindings.Add(path);
|
||||
}
|
||||
}
|
||||
|
||||
var orderedFindings = findings
|
||||
.OrderByDescending(static finding => GetSeverityWeight(finding.Level))
|
||||
.ThenBy(static finding => finding.Path, StringComparer.Ordinal)
|
||||
.ThenBy(static finding => finding.StartLine)
|
||||
.ThenBy(static finding => finding.RuleId, StringComparer.Ordinal)
|
||||
.ThenBy(static finding => finding.Id, StringComparer.Ordinal)
|
||||
.Take(configuration.MaxFindings)
|
||||
.ToImmutableArray();
|
||||
|
||||
var summary = BuildSummary(
|
||||
orderedFindings,
|
||||
files.Length,
|
||||
filesWithFindings.Count,
|
||||
attributionFiles.Count);
|
||||
|
||||
var status = DetermineStatus(orderedFindings);
|
||||
_logger.LogInformation(
|
||||
"AI Code Guard run completed for {Owner}/{Repo}@{Sha}: {Status}, findings={Count}",
|
||||
request.Owner,
|
||||
request.Repo,
|
||||
request.CommitSha,
|
||||
status,
|
||||
orderedFindings.Length);
|
||||
|
||||
return Task.FromResult(new AiCodeGuardRunResponse
|
||||
{
|
||||
Status = status,
|
||||
Summary = summary,
|
||||
Findings = orderedFindings,
|
||||
Configuration = configuration,
|
||||
});
|
||||
}
|
||||
|
||||
private static ImmutableArray<SecretRule> BuildSecretRules(AiCodeGuardRunConfiguration configuration)
|
||||
{
|
||||
var builder = ImmutableArray.CreateBuilder<SecretRule>(BuiltInSecretRules.Length + configuration.CustomSecretPatterns.Length);
|
||||
builder.AddRange(BuiltInSecretRules);
|
||||
|
||||
for (var index = 0; index < configuration.CustomSecretPatterns.Length; index++)
|
||||
{
|
||||
var pattern = configuration.CustomSecretPatterns[index];
|
||||
var ruleId = $"AICG-SECRET-CUSTOM-{index + 1:D2}";
|
||||
builder.Add(new SecretRule(
|
||||
ruleId,
|
||||
"Secrets",
|
||||
$"Custom secret pattern matched ({ruleId}).",
|
||||
new Regex(pattern, RegexOptions.CultureInvariant),
|
||||
AnnotationLevel.Failure,
|
||||
0.90));
|
||||
}
|
||||
|
||||
return builder.ToImmutable();
|
||||
}
|
||||
|
||||
private static void ScanSecrets(
|
||||
string path,
|
||||
IReadOnlyList<string> lines,
|
||||
ImmutableArray<SecretRule> rules,
|
||||
ICollection<AiCodeGuardFindingAnnotation> findings)
|
||||
{
|
||||
for (var lineNumber = 0; lineNumber < lines.Count; lineNumber++)
|
||||
{
|
||||
var line = lines[lineNumber];
|
||||
if (string.IsNullOrWhiteSpace(line))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (var rule in rules)
|
||||
{
|
||||
if (!rule.Pattern.IsMatch(line))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
findings.Add(CreateFinding(
|
||||
rule.RuleId,
|
||||
path,
|
||||
lineNumber + 1,
|
||||
rule.Level,
|
||||
rule.Category,
|
||||
rule.Message,
|
||||
rule.Confidence));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ScanAttribution(
|
||||
string path,
|
||||
IReadOnlyList<string> lines,
|
||||
ICollection<AiCodeGuardFindingAnnotation> findings,
|
||||
ISet<string> attributionFiles)
|
||||
{
|
||||
for (var lineNumber = 0; lineNumber < lines.Count; lineNumber++)
|
||||
{
|
||||
var line = lines[lineNumber];
|
||||
foreach (var marker in AttributionMarkers)
|
||||
{
|
||||
if (line.IndexOf(marker, StringComparison.OrdinalIgnoreCase) < 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
attributionFiles.Add(path);
|
||||
findings.Add(CreateFinding(
|
||||
"AICG-ATTRIBUTION-MARKER",
|
||||
path,
|
||||
lineNumber + 1,
|
||||
AnnotationLevel.Warning,
|
||||
"Attribution",
|
||||
$"AI attribution marker '{marker}' detected.",
|
||||
0.80));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ScanLicense(
|
||||
string path,
|
||||
IReadOnlyList<string> lines,
|
||||
ImmutableArray<string> allowedLicenses,
|
||||
ICollection<AiCodeGuardFindingAnnotation> findings)
|
||||
{
|
||||
var upperBound = Math.Min(lines.Count, 20);
|
||||
var discoveredLicense = default(string);
|
||||
var discoveredLine = 1;
|
||||
|
||||
for (var lineNumber = 0; lineNumber < upperBound; lineNumber++)
|
||||
{
|
||||
var match = SpdxLicenseRegex.Match(lines[lineNumber]);
|
||||
if (!match.Success)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
discoveredLicense = match.Groups["id"].Value.Trim();
|
||||
discoveredLine = lineNumber + 1;
|
||||
break;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(discoveredLicense))
|
||||
{
|
||||
findings.Add(CreateFinding(
|
||||
"AICG-LICENSE-MISSING",
|
||||
path,
|
||||
1,
|
||||
AnnotationLevel.Warning,
|
||||
"License",
|
||||
"SPDX-License-Identifier header is missing.",
|
||||
0.85));
|
||||
return;
|
||||
}
|
||||
|
||||
if (allowedLicenses.Length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var isAllowed = allowedLicenses.Contains(discoveredLicense, StringComparer.OrdinalIgnoreCase);
|
||||
if (isAllowed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
findings.Add(CreateFinding(
|
||||
"AICG-LICENSE-DISALLOWED",
|
||||
path,
|
||||
discoveredLine,
|
||||
AnnotationLevel.Failure,
|
||||
"License",
|
||||
$"SPDX license '{discoveredLicense}' is not in the allow list.",
|
||||
0.92));
|
||||
}
|
||||
|
||||
private static AiCodeGuardSummary BuildSummary(
|
||||
ImmutableArray<AiCodeGuardFindingAnnotation> findings,
|
||||
int filesAnalyzed,
|
||||
int filesWithFindings,
|
||||
int attributionFileCount)
|
||||
{
|
||||
var critical = findings.Count(static finding => finding.Level == AnnotationLevel.Failure && finding.Category == "Secrets");
|
||||
var high = findings.Count(static finding => finding.Level == AnnotationLevel.Failure && finding.Category != "Secrets");
|
||||
var medium = findings.Count(static finding => finding.Level == AnnotationLevel.Warning);
|
||||
var info = findings.Count(static finding => finding.Level == AnnotationLevel.Notice);
|
||||
|
||||
double? aiGeneratedPercentage = filesAnalyzed == 0
|
||||
? null
|
||||
: Math.Round((double)attributionFileCount * 100 / filesAnalyzed, 1, MidpointRounding.AwayFromZero);
|
||||
|
||||
return new AiCodeGuardSummary
|
||||
{
|
||||
TotalFindings = findings.Length,
|
||||
Critical = critical,
|
||||
High = high,
|
||||
Medium = medium,
|
||||
Low = 0,
|
||||
Info = info,
|
||||
AiGeneratedPercentage = aiGeneratedPercentage,
|
||||
FilesWithFindings = filesWithFindings,
|
||||
FilesAnalyzed = filesAnalyzed,
|
||||
};
|
||||
}
|
||||
|
||||
private static AiCodeGuardAnalysisStatus DetermineStatus(ImmutableArray<AiCodeGuardFindingAnnotation> findings)
|
||||
{
|
||||
if (findings.Any(static finding => finding.Level == AnnotationLevel.Failure))
|
||||
{
|
||||
return AiCodeGuardAnalysisStatus.Fail;
|
||||
}
|
||||
|
||||
if (findings.Any(static finding => finding.Level == AnnotationLevel.Warning))
|
||||
{
|
||||
return AiCodeGuardAnalysisStatus.Warning;
|
||||
}
|
||||
|
||||
return AiCodeGuardAnalysisStatus.Pass;
|
||||
}
|
||||
|
||||
private static int GetSeverityWeight(AnnotationLevel level)
|
||||
{
|
||||
return level switch
|
||||
{
|
||||
AnnotationLevel.Failure => 3,
|
||||
AnnotationLevel.Warning => 2,
|
||||
AnnotationLevel.Notice => 1,
|
||||
_ => 0,
|
||||
};
|
||||
}
|
||||
|
||||
private static string NormalizePath(string path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path))
|
||||
{
|
||||
throw new ArgumentException("Source file path is required.", nameof(path));
|
||||
}
|
||||
|
||||
return path.Trim().Replace('\\', '/');
|
||||
}
|
||||
|
||||
private static IReadOnlyList<string> GetLines(string content)
|
||||
{
|
||||
return (content ?? string.Empty)
|
||||
.Replace("\r\n", "\n", StringComparison.Ordinal)
|
||||
.Split('\n');
|
||||
}
|
||||
|
||||
private static AiCodeGuardFindingAnnotation CreateFinding(
|
||||
string ruleId,
|
||||
string path,
|
||||
int line,
|
||||
AnnotationLevel level,
|
||||
string category,
|
||||
string message,
|
||||
double confidence)
|
||||
{
|
||||
var canonical = $"{ruleId}|{path}|{line}|{message}";
|
||||
var hash = SHA256.HashData(Encoding.UTF8.GetBytes(canonical));
|
||||
var findingId = Convert.ToHexString(hash).ToLowerInvariant()[..16];
|
||||
|
||||
return new AiCodeGuardFindingAnnotation
|
||||
{
|
||||
Id = findingId,
|
||||
Path = path,
|
||||
StartLine = line,
|
||||
EndLine = line,
|
||||
Level = level,
|
||||
Category = category,
|
||||
Message = message,
|
||||
RuleId = ruleId,
|
||||
Confidence = confidence,
|
||||
};
|
||||
}
|
||||
|
||||
private sealed record SecretRule(
|
||||
string RuleId,
|
||||
string Category,
|
||||
string Message,
|
||||
Regex Pattern,
|
||||
AnnotationLevel Level,
|
||||
double Confidence);
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using StellaOps.Integrations.Contracts;
|
||||
using StellaOps.Integrations.Contracts.AiCodeGuard;
|
||||
using StellaOps.Integrations.Core;
|
||||
using StellaOps.Integrations.WebService.AiCodeGuard;
|
||||
|
||||
namespace StellaOps.Integrations.WebService;
|
||||
|
||||
@@ -14,6 +16,18 @@ public static class IntegrationEndpoints
|
||||
var group = app.MapGroup("/api/v1/integrations")
|
||||
.WithTags("Integrations");
|
||||
|
||||
// Standalone AI Code Guard run
|
||||
group.MapPost("/ai-code-guard/run", async (
|
||||
[FromServices] IAiCodeGuardRunService aiCodeGuardRunService,
|
||||
[FromBody] AiCodeGuardRunRequest request,
|
||||
CancellationToken cancellationToken) =>
|
||||
{
|
||||
var response = await aiCodeGuardRunService.RunAsync(request, cancellationToken);
|
||||
return Results.Ok(response);
|
||||
})
|
||||
.WithName("RunAiCodeGuard")
|
||||
.WithDescription("Runs standalone AI Code Guard checks (equivalent to stella guard run).");
|
||||
|
||||
// List integrations
|
||||
group.MapGet("/", async (
|
||||
[FromServices] IntegrationService service,
|
||||
|
||||
@@ -2,6 +2,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
using StellaOps.Auth.ServerIntegration;
|
||||
using StellaOps.Integrations.Persistence;
|
||||
using StellaOps.Integrations.WebService;
|
||||
using StellaOps.Integrations.WebService.AiCodeGuard;
|
||||
using StellaOps.Integrations.WebService.Infrastructure;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
@@ -51,6 +52,8 @@ builder.Services.AddScoped<IAuthRefResolver, StubAuthRefResolver>();
|
||||
|
||||
// Core service
|
||||
builder.Services.AddScoped<IntegrationService>();
|
||||
builder.Services.AddSingleton<IAiCodeGuardPipelineConfigLoader, AiCodeGuardPipelineConfigLoader>();
|
||||
builder.Services.AddScoped<IAiCodeGuardRunService, AiCodeGuardRunService>();
|
||||
|
||||
builder.Services.AddStellaOpsCors(builder.Environment, builder.Configuration);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# StellaOps.Integrations.WebService Task Board
|
||||
# StellaOps.Integrations.WebService Task Board
|
||||
This board mirrors active sprint tasks for this module.
|
||||
Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_solid_review.md`.
|
||||
|
||||
@@ -6,3 +6,7 @@ Source of truth: `docs/implplan/SPRINT_20260130_002_Tools_csproj_remediation_sol
|
||||
| --- | --- | --- |
|
||||
| REMED-05 | TODO | Remediation checklist: docs/implplan/audits/csproj-standards/remediation/checklists/src/Integrations/StellaOps.Integrations.WebService/StellaOps.Integrations.WebService.md. |
|
||||
| REMED-06 | DONE | SOLID review notes captured for SPRINT_20260130_002. |
|
||||
|
||||
| SPRINT_20260208_040-WEB | DONE | AI Code Guard run endpoint + service wiring in Integrations WebService. |
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user