From f79bc2cc02d579841e9042d24eb4d2e1bc9596f9 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Tue, 2 Jun 2026 10:21:11 +0900 Subject: [PATCH 1/4] Bound GitHub issue scrub input (#2888) --- changelog.d/unreleased/2888.security.md | 16 +++++ src/CodeIndex/Cli/GitHubIssueReporter.cs | 63 ++++++++++++++++--- .../GitHubIssueReporterTests.cs | 35 +++++++++++ 3 files changed, 105 insertions(+), 9 deletions(-) create mode 100644 changelog.d/unreleased/2888.security.md diff --git a/changelog.d/unreleased/2888.security.md b/changelog.d/unreleased/2888.security.md new file mode 100644 index 0000000000..4db98989cb --- /dev/null +++ b/changelog.d/unreleased/2888.security.md @@ -0,0 +1,16 @@ +--- +category: security +issues: + - 2888 +affected: + - src/CodeIndex/Cli/GitHubIssueReporter.cs + - tests/CodeIndex.Tests/GitHubIssueReporterTests.cs +--- + +## English + +- **GitHub suggestion submission now bounds code scrubbing input (#2888)** — outbound issue title/body scrubbing uses a capped linear scanner for fenced and inline code, preventing very large suggestion text from driving unbounded regex work or leaking truncated code spans. + +## 日本語 + +- **GitHub 提案送信時のコード scrub 入力を上限付きにしました (#2888)** — 外部送信用 Issue のタイトル/本文 scrub は上限付きの線形スキャナで fenced code と inline code を処理し、巨大な提案本文による無制限の regex 処理や切り詰め途中の code span 漏えいを防ぎます。 diff --git a/src/CodeIndex/Cli/GitHubIssueReporter.cs b/src/CodeIndex/Cli/GitHubIssueReporter.cs index 76bcf8dcb3..2fac0f3f5d 100644 --- a/src/CodeIndex/Cli/GitHubIssueReporter.cs +++ b/src/CodeIndex/Cli/GitHubIssueReporter.cs @@ -49,6 +49,9 @@ internal static class GitHubIssueReporter private static readonly TimeSpan DefaultRateLimitRetryDelay = TimeSpan.FromMinutes(1); private const string TimeoutEnvironmentVariable = "CDIDX_GITHUB_SUBMIT_TIMEOUT_SECONDS"; internal const int MaxGitHubIssueTitleLength = 255; + internal const int MaxScrubInputLength = 16 * 1024; + private const string CodeExampleRemovedText = "[code example removed]"; + private const string ScrubInputTruncatedText = "\n[truncated]"; // Static HttpClient singleton — .NET best practice for reuse. // 静的 HttpClient シングルトン — .NET の再利用ベストプラクティス。 @@ -456,12 +459,55 @@ internal static string ScrubInlineCode(string text) if (string.IsNullOrEmpty(text)) return text; - var scrubbed = Regex.Replace( - text, - @"(?s)```.*?```", - "[code example removed]"); + var (boundedText, wasTruncated) = BoundScrubInput(text); + var scrubbed = ScrubFencedCodeBlocks(boundedText); + scrubbed = ScrubSingleBacktickSpans(scrubbed); - return ScrubSingleBacktickSpans(scrubbed); + return wasTruncated + ? scrubbed + ScrubInputTruncatedText + : scrubbed; + } + + private static (string Text, bool WasTruncated) BoundScrubInput(string text) => + text.Length <= MaxScrubInputLength + ? (text, false) + : (text[..MaxScrubInputLength], true); + + private static string ScrubFencedCodeBlocks(string text) + { + var builder = new StringBuilder(text.Length); + var index = 0; + while (index < text.Length) + { + var open = FindTripleBacktickFence(text, index); + if (open < 0) + { + builder.Append(text, index, text.Length - index); + break; + } + + builder.Append(text, index, open - index); + builder.Append(CodeExampleRemovedText); + + var close = FindTripleBacktickFence(text, open + 3); + if (close < 0) + break; + + index = close + 3; + } + + return builder.ToString(); + } + + private static int FindTripleBacktickFence(string text, int start) + { + for (var i = start; i + 2 < text.Length; i++) + { + if (text[i] == '`' && text[i + 1] == '`' && text[i + 2] == '`') + return i; + } + + return -1; } internal static string BuildIssueTitle(string category, string description) @@ -534,12 +580,11 @@ private static string ScrubSingleBacktickSpans(string text) var close = FindInlineCodeClose(text, index + 1); if (close < 0) { - builder.Append(text[index]); - index++; - continue; + builder.Append(CodeExampleRemovedText); + break; } - builder.Append("[code example removed]"); + builder.Append(CodeExampleRemovedText); index = close + 1; } diff --git a/tests/CodeIndex.Tests/GitHubIssueReporterTests.cs b/tests/CodeIndex.Tests/GitHubIssueReporterTests.cs index 00725eb15c..2462f77403 100644 --- a/tests/CodeIndex.Tests/GitHubIssueReporterTests.cs +++ b/tests/CodeIndex.Tests/GitHubIssueReporterTests.cs @@ -256,6 +256,41 @@ public void ScrubInlineCode_HandlesEmptyAndNull() Assert.Null(GitHubIssueReporter.ScrubInlineCode(null!)); } + [Fact] + public void ScrubInlineCode_BoundsLargePlainTextInput() + { + var input = new string('a', GitHubIssueReporter.MaxScrubInputLength + 1024); + + var result = GitHubIssueReporter.ScrubInlineCode(input); + + Assert.Equal(GitHubIssueReporter.MaxScrubInputLength + "\n[truncated]".Length, result.Length); + Assert.EndsWith("\n[truncated]", result); + } + + [Fact] + public void ScrubInlineCode_BoundsLargeUnclosedFenceAndDoesNotLeakCode() + { + var input = "Before\n```csharp\nsecret();\n" + new string('x', GitHubIssueReporter.MaxScrubInputLength + 1024); + + var result = GitHubIssueReporter.ScrubInlineCode(input); + + Assert.Equal("Before\n[code example removed]\n[truncated]", result); + Assert.DoesNotContain("secret", result); + Assert.DoesNotContain("```", result); + } + + [Fact] + public void ScrubInlineCode_BoundsLargeUnclosedInlineSpanAndDoesNotLeakCode() + { + var input = "Before `secret()" + new string('x', GitHubIssueReporter.MaxScrubInputLength + 1024); + + var result = GitHubIssueReporter.ScrubInlineCode(input); + + Assert.Equal("Before [code example removed]\n[truncated]", result); + Assert.DoesNotContain("secret", result); + Assert.DoesNotContain("`", result); + } + [Fact] public void BuildSubmissionFailureMessage_IsActionable() { From 99c78f23b07d7dee43188f15fa577fc4b0604446 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Tue, 2 Jun 2026 10:44:26 +0900 Subject: [PATCH 2/4] Sanitize GitHub API error bodies (#2855) --- changelog.d/unreleased/2855.security.md | 16 ++ src/CodeIndex/Cli/GitHubIssueReporter.cs | 134 +++++++++++++++- .../GitHubIssueReporterTests.cs | 145 ++++++++++++++++++ 3 files changed, 289 insertions(+), 6 deletions(-) create mode 100644 changelog.d/unreleased/2855.security.md diff --git a/changelog.d/unreleased/2855.security.md b/changelog.d/unreleased/2855.security.md new file mode 100644 index 0000000000..c63e111e3e --- /dev/null +++ b/changelog.d/unreleased/2855.security.md @@ -0,0 +1,16 @@ +--- +category: security +issues: + - 2855 +affected: + - src/CodeIndex/Cli/GitHubIssueReporter.cs + - tests/CodeIndex.Tests/GitHubIssueReporterTests.cs +--- + +## English + +- **GitHub suggestion submission now bounds and redacts API error bodies (#2855)** — failed issue creation and rate-limit diagnostics read only a small response excerpt, redact sensitive JSON-like fields, and keep stderr plus persisted retry details bounded. + +## 日本語 + +- **GitHub 提案送信時の API エラー本文を上限付き・redaction 済みにしました (#2855)** — Issue 作成失敗や rate limit 診断では小さなレスポンス抜粋だけを読み、機密性のある JSON 風フィールドを redaction し、stderr と永続化される retry detail を bounded に保ちます。 diff --git a/src/CodeIndex/Cli/GitHubIssueReporter.cs b/src/CodeIndex/Cli/GitHubIssueReporter.cs index 2fac0f3f5d..0ddc4e018f 100644 --- a/src/CodeIndex/Cli/GitHubIssueReporter.cs +++ b/src/CodeIndex/Cli/GitHubIssueReporter.cs @@ -50,8 +50,15 @@ internal static class GitHubIssueReporter private const string TimeoutEnvironmentVariable = "CDIDX_GITHUB_SUBMIT_TIMEOUT_SECONDS"; internal const int MaxGitHubIssueTitleLength = 255; internal const int MaxScrubInputLength = 16 * 1024; + internal const int MaxGitHubApiErrorBodyBytes = 4 * 1024; + private const int MaxGitHubApiErrorDetailLength = 500; private const string CodeExampleRemovedText = "[code example removed]"; private const string ScrubInputTruncatedText = "\n[truncated]"; + private const string ApiErrorBodyTruncatedText = " [response body truncated]"; + private static readonly Regex SensitiveJsonFieldPattern = new( + "(\"(?:token|access_token|authorization|password|secret|client_secret|private_key|api_key)\"\\s*:\\s*)(\"(?:\\\\.|[^\"])*\"|[^,}\\]\\s]+)", + RegexOptions.IgnoreCase | RegexOptions.CultureInvariant, + TimeSpan.FromMilliseconds(100)); // Static HttpClient singleton — .NET best practice for reuse. // 静的 HttpClient シングルトン — .NET の再利用ベストプラクティス。 @@ -419,7 +426,7 @@ private static bool IsHexHash(string value) if (!response.IsSuccessStatusCode) { - var errorBody = await response.Content.ReadAsStringAsync(cancellationToken); + var errorBody = await ReadBoundedApiErrorBodyAsync(response.Content, cancellationToken); var rateLimitRetryAt = GetRateLimitRetryAt(response, DateTime.UtcNow); if (rateLimitRetryAt != null) { @@ -643,19 +650,134 @@ internal static string BuildSubmissionFailureMessage(string detail) => $"[cdidx] GitHub issue creation failed: {detail}. The suggestion stays recorded locally; check `CDIDX_GITHUB_TOKEN`, network access, and proxy environment variables (`HTTPS_PROXY`, `HTTP_PROXY`, `ALL_PROXY`, `NO_PROXY`), then retry `suggest_improvement` when ready."; internal static string BuildApiFailureMessage(int statusCode, string errorBody) => - $"[cdidx] GitHub API responded {statusCode}: {errorBody}. GitHub submission was skipped; the suggestion stays local. Check `CDIDX_GITHUB_TOKEN`, repository permissions, or network access, then retry `suggest_improvement`."; + $"[cdidx] GitHub API responded {BuildApiErrorDetail(statusCode, errorBody)}. GitHub submission was skipped; the suggestion stays local. Check `CDIDX_GITHUB_TOKEN`, repository permissions, or network access, then retry `suggest_improvement`."; internal static string BuildRateLimitFailureMessage(int statusCode, string errorBody, DateTime nextRetryAt) => - $"[cdidx] GitHub API rate limit response {statusCode}: {errorBody}. GitHub submission was paused until {nextRetryAt:O}; the suggestion stays local and will not be retried before then."; + $"[cdidx] GitHub API rate limit response {BuildApiErrorDetail(statusCode, errorBody)}. GitHub submission was paused until {nextRetryAt:O}; the suggestion stays local and will not be retried before then."; internal static string BuildApiErrorDetail(int statusCode, string errorBody) { - var normalized = errorBody.Replace("\r", " ").Replace("\n", " ").Trim(); - if (normalized.Length > 500) - normalized = normalized[..500] + "..."; + var normalized = SanitizeApiErrorBody(errorBody); + if (normalized.Length > MaxGitHubApiErrorDetailLength) + normalized = TruncateWithEllipsis(normalized, MaxGitHubApiErrorDetailLength); return $"{statusCode}: {normalized}"; } + private static async Task ReadBoundedApiErrorBodyAsync(HttpContent content, CancellationToken cancellationToken) + { + await using var stream = await content.ReadAsStreamAsync(cancellationToken); + return await ReadBoundedApiErrorBodyAsync(stream, cancellationToken); + } + + internal static async Task ReadBoundedApiErrorBodyAsync(Stream stream, CancellationToken cancellationToken) + { + var buffer = new byte[MaxGitHubApiErrorBodyBytes + 1]; + var total = 0; + while (total < buffer.Length) + { + var read = await stream.ReadAsync(buffer.AsMemory(total, buffer.Length - total), cancellationToken); + if (read == 0) + break; + total += read; + } + + var bodyLength = Math.Min(total, MaxGitHubApiErrorBodyBytes); + var body = Encoding.UTF8.GetString(buffer, 0, bodyLength); + return total > MaxGitHubApiErrorBodyBytes + ? body + ApiErrorBodyTruncatedText + : body; + } + + private static string SanitizeApiErrorBody(string errorBody) + { + var bounded = BoundApiErrorBodyForFormatting(errorBody); + var sanitized = TryRedactSensitiveJsonFields(bounded, out var redactedJson) + ? redactedJson + : RedactSensitiveJsonLikeFields(bounded); + var normalized = sanitized.Replace("\r", " ").Replace("\n", " ").Trim(); + return normalized.Length == 0 ? "" : normalized; + } + + private static string BoundApiErrorBodyForFormatting(string errorBody) + { + if (string.IsNullOrEmpty(errorBody)) + return string.Empty; + + return errorBody.Length <= MaxGitHubApiErrorBodyBytes + ? errorBody + : errorBody[..MaxGitHubApiErrorBodyBytes] + ApiErrorBodyTruncatedText; + } + + private static bool TryRedactSensitiveJsonFields(string errorBody, out string redactedJson) + { + redactedJson = errorBody; + try + { + var node = JsonNode.Parse(errorBody); + if (node == null || !RedactSensitiveJsonFields(node)) + return false; + + redactedJson = node.ToJsonString(); + return true; + } + catch (JsonException) + { + return false; + } + } + + private static bool RedactSensitiveJsonFields(JsonNode node) + { + var changed = false; + if (node is JsonObject obj) + { + foreach (var property in obj.ToArray()) + { + if (IsSensitiveApiErrorField(property.Key)) + { + obj[property.Key] = "[redacted]"; + changed = true; + continue; + } + + if (property.Value != null) + changed |= RedactSensitiveJsonFields(property.Value); + } + } + else if (node is JsonArray array) + { + foreach (var item in array) + { + if (item != null) + changed |= RedactSensitiveJsonFields(item); + } + } + + return changed; + } + + private static bool IsSensitiveApiErrorField(string fieldName) => + fieldName.Contains("token", StringComparison.OrdinalIgnoreCase) + || fieldName.Contains("secret", StringComparison.OrdinalIgnoreCase) + || fieldName.Contains("password", StringComparison.OrdinalIgnoreCase) + || fieldName.Equals("authorization", StringComparison.OrdinalIgnoreCase) + || fieldName.Equals("api_key", StringComparison.OrdinalIgnoreCase) + || fieldName.Equals("private_key", StringComparison.OrdinalIgnoreCase); + + private static string RedactSensitiveJsonLikeFields(string errorBody) + { + try + { + return SensitiveJsonFieldPattern.Replace( + errorBody, + match => match.Groups[1].Value + "\"[redacted]\""); + } + catch (RegexMatchTimeoutException) + { + return "[response body omitted after redaction timeout]"; + } + } + internal static string BuildRateLimitErrorDetail(int statusCode, string errorBody, DateTime nextRetryAt) => $"{BuildApiErrorDetail(statusCode, errorBody)}; next_retry_at={nextRetryAt:O}"; diff --git a/tests/CodeIndex.Tests/GitHubIssueReporterTests.cs b/tests/CodeIndex.Tests/GitHubIssueReporterTests.cs index 2462f77403..ecb6f92cd2 100644 --- a/tests/CodeIndex.Tests/GitHubIssueReporterTests.cs +++ b/tests/CodeIndex.Tests/GitHubIssueReporterTests.cs @@ -314,6 +314,39 @@ public void BuildApiFailureMessage_IsActionable() Assert.Contains("retry `suggest_improvement`", message); } + [Fact] + public void BuildApiFailureMessage_RedactsAndBoundsSensitiveErrorBody() + { + var errorBody = $$""" + { + "message": "validation failed", + "token": "ghp_secret_token", + "details": "{{new string('x', 2000)}}" + } + """; + + var message = GitHubIssueReporter.BuildApiFailureMessage(403, errorBody); + + Assert.DoesNotContain("ghp_secret_token", message); + Assert.Contains("\"token\":\"[redacted]\"", message); + Assert.True(message.Length < 900); + } + + [Fact] + public void BuildRateLimitFailureMessage_RedactsSensitiveErrorBody() + { + var retryAt = new DateTime(2026, 5, 23, 10, 0, 0, DateTimeKind.Utc); + + var message = GitHubIssueReporter.BuildRateLimitFailureMessage( + 429, + """{ "authorization": "Bearer ghp_secret_token", "message": "rate limited" }""", + retryAt); + + Assert.DoesNotContain("ghp_secret_token", message); + Assert.Contains("\"authorization\":\"[redacted]\"", message); + Assert.Contains(retryAt.ToString("O"), message); + } + [Fact] public void BuildIssueLabels_MapsSuggestionCategoriesToExistingRepositoryLabels() { @@ -655,6 +688,64 @@ public async Task TryCreateIssueDetailedAsync_CreateApiFails_ReturnsDiagnosticEr } } + [Fact] + public async Task TryCreateIssueDetailedAsync_CreateApiFails_ReadsBoundedSanitizedErrorBody() + { + _env.Set("CDIDX_GITHUB_TOKEN", "ghp_idempotency_test"); + + var errorBody = "{\"message\":\"validation failed\",\"token\":\"ghp_secret_token\",\"details\":\"" + + new string('x', GitHubIssueReporter.MaxGitHubApiErrorBodyBytes * 2) + + "\"}"; + + var handler = new RecordingHandler(); + handler.AddResponse(req => req.Method == HttpMethod.Get && req.RequestUri!.AbsolutePath == "/search/issues", + new HttpResponseMessage(HttpStatusCode.OK) + { + Content = MakeJsonContent("""{ "total_count": 0, "items": [] }"""), + }); + handler.AddResponse(req => req.Method == HttpMethod.Get && req.RequestUri!.AbsolutePath == "/repos/widthdom/CodeIndex/issues", + new HttpResponseMessage(HttpStatusCode.OK) + { + Content = MakeJsonContent("[]"), + }); + handler.AddResponse(req => req.Method == HttpMethod.Post && req.RequestUri!.AbsolutePath.Contains("/issues"), + new HttpResponseMessage(HttpStatusCode.UnprocessableEntity) + { + Content = new ByteArrayContent(Encoding.UTF8.GetBytes(errorBody)), + }); + using var mockClient = new HttpClient(handler); + GitHubIssueReporter.s_httpClientOverride = mockClient; + try + { + var record = MakeRecordWithKnownHash(); + var result = await GitHubIssueReporter.TryCreateIssueDetailedAsync(record, "1.0.0-test"); + + Assert.Null(result.IssueUrl); + Assert.DoesNotContain("ghp_secret_token", result.Error); + Assert.Contains("[redacted]", result.Error); + Assert.True(result.Error!.Length <= 512); + } + finally + { + GitHubIssueReporter.s_httpClientOverride = null; + } + } + + [Fact] + public async Task ReadBoundedApiErrorBodyAsync_StopsAfterByteCap() + { + var errorBody = Encoding.UTF8.GetBytes( + "prefix " + new string('x', GitHubIssueReporter.MaxGitHubApiErrorBodyBytes * 2)); + var stream = new ThrowIfOverReadStream( + errorBody, + GitHubIssueReporter.MaxGitHubApiErrorBodyBytes + 1); + + var result = await GitHubIssueReporter.ReadBoundedApiErrorBodyAsync(stream, CancellationToken.None); + + Assert.True(stream.BytesRead <= GitHubIssueReporter.MaxGitHubApiErrorBodyBytes + 1); + Assert.EndsWith(" [response body truncated]", result); + } + [Fact] public async Task TryCreateIssueDetailedAsync_UserCancellation_Propagates() { @@ -884,6 +975,60 @@ protected override Task SendAsync(HttpRequestMessage reques } } + private sealed class ThrowIfOverReadStream(byte[] data, int maxBytesRead) : Stream + { + private int _position; + + public int BytesRead { get; private set; } + public override bool CanRead => true; + public override bool CanSeek => false; + public override bool CanWrite => false; + public override long Length => data.Length; + + public override long Position + { + get => _position; + set => throw new NotSupportedException(); + } + + public override int Read(byte[] buffer, int offset, int count) => + ReadCore(buffer.AsSpan(offset, count)); + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => + Task.FromResult(ReadCore(buffer.AsSpan(offset, count))); + + public override ValueTask ReadAsync(Memory buffer, CancellationToken cancellationToken = default) => + ValueTask.FromResult(ReadCore(buffer.Span)); + + private int ReadCore(Span destination) + { + if (_position >= data.Length) + return 0; + + var count = Math.Min(destination.Length, data.Length - _position); + if (BytesRead + count > maxBytesRead) + throw new InvalidOperationException("Response body read exceeded bounded limit."); + + data.AsSpan(_position, count).CopyTo(destination); + _position += count; + BytesRead += count; + return count; + } + + public override void Flush() + { + } + + public override long Seek(long offset, SeekOrigin origin) => + throw new NotSupportedException(); + + public override void SetLength(long value) => + throw new NotSupportedException(); + + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException(); + } + private sealed class DelayingHandler(TimeSpan delay) : HttpMessageHandler { protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) From 82f9516c74bbf449564817ce2ebaca6b78d3aa8f Mon Sep 17 00:00:00 2001 From: Widthdom Date: Tue, 2 Jun 2026 15:21:13 +0900 Subject: [PATCH 3/4] Avoid token-shaped test fixture (#2855) --- tests/CodeIndex.Tests/GitHubIssueReporterTests.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/CodeIndex.Tests/GitHubIssueReporterTests.cs b/tests/CodeIndex.Tests/GitHubIssueReporterTests.cs index ecb6f92cd2..0bdb59aec9 100644 --- a/tests/CodeIndex.Tests/GitHubIssueReporterTests.cs +++ b/tests/CodeIndex.Tests/GitHubIssueReporterTests.cs @@ -320,14 +320,14 @@ public void BuildApiFailureMessage_RedactsAndBoundsSensitiveErrorBody() var errorBody = $$""" { "message": "validation failed", - "token": "ghp_secret_token", + "token": "value-that-must-not-leak", "details": "{{new string('x', 2000)}}" } """; var message = GitHubIssueReporter.BuildApiFailureMessage(403, errorBody); - Assert.DoesNotContain("ghp_secret_token", message); + Assert.DoesNotContain("value-that-must-not-leak", message); Assert.Contains("\"token\":\"[redacted]\"", message); Assert.True(message.Length < 900); } @@ -339,10 +339,10 @@ public void BuildRateLimitFailureMessage_RedactsSensitiveErrorBody() var message = GitHubIssueReporter.BuildRateLimitFailureMessage( 429, - """{ "authorization": "Bearer ghp_secret_token", "message": "rate limited" }""", + """{ "authorization": "Bearer value-that-must-not-leak", "message": "rate limited" }""", retryAt); - Assert.DoesNotContain("ghp_secret_token", message); + Assert.DoesNotContain("value-that-must-not-leak", message); Assert.Contains("\"authorization\":\"[redacted]\"", message); Assert.Contains(retryAt.ToString("O"), message); } @@ -693,7 +693,7 @@ public async Task TryCreateIssueDetailedAsync_CreateApiFails_ReadsBoundedSanitiz { _env.Set("CDIDX_GITHUB_TOKEN", "ghp_idempotency_test"); - var errorBody = "{\"message\":\"validation failed\",\"token\":\"ghp_secret_token\",\"details\":\"" + var errorBody = "{\"message\":\"validation failed\",\"token\":\"value-that-must-not-leak\",\"details\":\"" + new string('x', GitHubIssueReporter.MaxGitHubApiErrorBodyBytes * 2) + "\"}"; @@ -721,7 +721,7 @@ public async Task TryCreateIssueDetailedAsync_CreateApiFails_ReadsBoundedSanitiz var result = await GitHubIssueReporter.TryCreateIssueDetailedAsync(record, "1.0.0-test"); Assert.Null(result.IssueUrl); - Assert.DoesNotContain("ghp_secret_token", result.Error); + Assert.DoesNotContain("value-that-must-not-leak", result.Error); Assert.Contains("[redacted]", result.Error); Assert.True(result.Error!.Length <= 512); } From c2850c6184c0746845e81aab27f7fe1af3786cb7 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Tue, 2 Jun 2026 19:01:19 +0900 Subject: [PATCH 4/4] Allow CI artifact overwrite on reruns --- .github/workflows/dotnet.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index ac3e8f7008..441227b5f3 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -240,6 +240,7 @@ jobs: with: name: TestResults-${{ matrix.os }}-${{ matrix.test-framework }} if-no-files-found: warn + overwrite: true path: | TestResults/**/*.trx TestResults/**/*.txt @@ -255,6 +256,7 @@ jobs: with: name: Coverage-${{ matrix.os }}-${{ matrix.test-framework }} if-no-files-found: warn + overwrite: true path: TestResults/**/coverage.cobertura.xml - name: Publish @@ -266,4 +268,5 @@ jobs: uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: CodeIndex + overwrite: true path: publish/**