-
Notifications
You must be signed in to change notification settings - Fork 82
Redact connection-string passwords in generated package docs #1410
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
932f6d2
Redact connection-string passwords in generated package docs
IEvangelist c9749cd
Address PR review: markdown-safe placeholder, cover example nodes, nu…
IEvangelist 458fab3
Use "Placeholder" redaction token per 1ES recommendation
radical 8ca47ae
fix(PackageJsonGenerator): tighten redaction regex and close doc gaps
radical File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
70 changes: 70 additions & 0 deletions
70
src/tools/PackageJsonGenerator/Helpers/DocumentationSanitizer.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| // Licensed to the .NET Foundation under one or more agreements. | ||
| // The .NET Foundation licenses this file to you under the MIT license. | ||
|
|
||
| using System.Text.RegularExpressions; | ||
|
|
||
| namespace PackageJsonGenerator.Helpers; | ||
|
|
||
| /// <summary> | ||
| /// Sanitizes documentation text copied verbatim from package XML docs before it | ||
| /// is emitted into the generated JSON data files. | ||
| /// </summary> | ||
| public static class DocumentationSanitizer | ||
| { | ||
| private const string PasswordPlaceholder = "Placeholder"; | ||
|
|
||
| // Matches connection-string style "key=value" credential pairs such as | ||
| // "User ID=sa;Password=password" or "Pwd=hunter2". The '=' is intentionally | ||
| // required with no surrounding whitespace so that C# default parameter | ||
| // values ("string? password = null") are never matched. The value (capture | ||
| // group 2) stops at connection-string / JSON delimiters (';', quotes, comma, | ||
| // backslash), at markdown / URI delimiters ('`', ')', ']', '&', '|') so a | ||
| // trailing token or inline-code fence is not swallowed, and at the '{'/'}' | ||
| // (and legacy '<'/'>') markers. A trailing '.' is trimmed off the value in | ||
| // the replacement callback (sentence terminators) rather than excluded from | ||
| // the class, which would truncate dotted values. Re-running over | ||
| // already-redacted text reproduces identical output, so replacement is | ||
| // idempotent. | ||
| private static readonly Regex ConnectionStringPasswordRegex = new( | ||
| "\\b(password|pwd)=([^;\"'{}<>\\s\\\\,`)\\]&|]+)", | ||
| RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled); | ||
|
|
||
| /// <summary> | ||
| /// Replaces literal password values inside connection-string style | ||
| /// "key=value" pairs with a <c>Placeholder</c> token. | ||
| /// </summary> | ||
| /// <remarks> | ||
| /// Documentation is copied verbatim from package XML docs and often contains | ||
| /// example connection strings (for example the SqlServer | ||
| /// <c>GetConnectionString</c> returns text | ||
| /// <c>"Server=host,port;User ID=sa;Password=password;TrustServerCertificate=true"</c>). | ||
| /// Those literal credential tokens are not real secrets, but they trip | ||
| /// push-time secret scanners such as CredScan <c>SqlLegacyCredentials</c> | ||
| /// (SEC101/037) when the generated JSON is mirrored to a protected remote. | ||
| /// The <c>Placeholder</c> token contains no markup or brace characters, so the | ||
| /// example still renders correctly when doc nodes are emitted as Markdown; it | ||
| /// is the redaction value recommended by 1ES for scrubbed credential examples. | ||
| /// </remarks> | ||
| /// <param name="text">The documentation text to sanitize.</param> | ||
| /// <returns>The text with connection-string password values redacted.</returns> | ||
| public static string? RedactConnectionStringPasswords(string? text) | ||
| { | ||
| if (string.IsNullOrEmpty(text) || !text.Contains('=')) | ||
| { | ||
| return text; | ||
| } | ||
|
|
||
| return ConnectionStringPasswordRegex.Replace( | ||
| text, | ||
| static match => | ||
| { | ||
| // Preserve a trailing sentence period that the value class | ||
| // intentionally consumes (dots are valid inside values, so they | ||
| // are trimmed here rather than excluded from the character class). | ||
| var value = match.Groups[2].Value; | ||
| var redacted = value.TrimEnd('.'); | ||
| var trailingDots = value[redacted.Length..]; | ||
| return $"{match.Groups[1].Value}={PasswordPlaceholder}{trailingDots}"; | ||
| }); | ||
| } | ||
| } |
86 changes: 86 additions & 0 deletions
86
tests/PackageJsonGenerator.Tests/DocumentationSanitizerTests.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| using PackageJsonGenerator.Helpers; | ||
|
|
||
| namespace PackageJsonGenerator.Tests; | ||
|
|
||
| public sealed class DocumentationSanitizerTests | ||
| { | ||
| [Fact] | ||
| public void RedactConnectionStringPasswords_RedactsSqlServerConnectionString() | ||
| { | ||
| var input = "A connection string for the SQL Server in the form \"Server=host,port;User ID=sa;Password=password;TrustServerCertificate=true\"."; | ||
|
|
||
| var result = DocumentationSanitizer.RedactConnectionStringPasswords(input); | ||
|
|
||
| Assert.Equal( | ||
| "A connection string for the SQL Server in the form \"Server=host,port;User ID=sa;Password=Placeholder;TrustServerCertificate=true\".", | ||
| result); | ||
| } | ||
|
|
||
| [Theory] | ||
| // Password at the end of the example (no trailing ';'). | ||
| [InlineData( | ||
| "Host=host;Port=port;Username=postgres;Password=password", | ||
| "Host=host;Port=port;Username=postgres;Password=Placeholder")] | ||
| // Keyword casing is preserved and the 'Pwd' alias is handled. | ||
| [InlineData("Server=s;Pwd=hunter2;Uid=admin", "Server=s;Pwd=Placeholder;Uid=admin")] | ||
| [InlineData("server=s;PASSWORD=S0meThing!;uid=a", "server=s;PASSWORD=Placeholder;uid=a")] | ||
| public void RedactConnectionStringPasswords_RedactsVariousFormats(string input, string expected) | ||
| { | ||
| Assert.Equal(expected, DocumentationSanitizer.RedactConnectionStringPasswords(input)); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void RedactConnectionStringPasswords_IsIdempotent() | ||
| { | ||
| var alreadyRedacted = "Server=host,port;User ID=sa;Password=Placeholder;TrustServerCertificate=true"; | ||
|
|
||
| var result = DocumentationSanitizer.RedactConnectionStringPasswords(alreadyRedacted); | ||
|
|
||
| Assert.Equal(alreadyRedacted, result); | ||
| } | ||
|
|
||
| [Fact] | ||
| public void RedactConnectionStringPasswords_UsesMarkdownSafePlaceholder() | ||
| { | ||
| // The placeholder must not contain angle brackets, which would be | ||
| // dropped as raw HTML when doc nodes are rendered to Markdown. | ||
| var result = DocumentationSanitizer.RedactConnectionStringPasswords("Password=password"); | ||
|
|
||
| Assert.Equal("Password=Placeholder", result); | ||
| Assert.DoesNotContain('<', result!); | ||
| Assert.DoesNotContain('>', result!); | ||
| } | ||
|
|
||
| [Theory] | ||
| // C# default parameter values use spaces around '=' and must not be touched. | ||
| [InlineData("public static IResourceBuilder<T> WithPassword<T>(this IResourceBuilder<T> b, string? password = null)")] | ||
| [InlineData("The password used to authenticate. Defaults to a generated value.")] | ||
| [InlineData("Gets the connection string for the resource.")] | ||
| public void RedactConnectionStringPasswords_LeavesNonConnectionStringTextUntouched(string input) | ||
| { | ||
| Assert.Equal(input, DocumentationSanitizer.RedactConnectionStringPasswords(input)); | ||
| } | ||
|
|
||
| [Theory] | ||
| [InlineData("")] | ||
| [InlineData(null)] | ||
| public void RedactConnectionStringPasswords_HandlesEmptyAndNull(string? input) | ||
| { | ||
| Assert.Equal(input, DocumentationSanitizer.RedactConnectionStringPasswords(input)); | ||
| } | ||
|
|
||
| [Theory] | ||
| // A following inline-code fence, link paren, table pipe, or bracket must not | ||
| // be swallowed into the redacted value; they delimit the value's end. | ||
| [InlineData("Use `Password=secret` in config.", "Use `Password=Placeholder` in config.")] | ||
| [InlineData("[docs](https://h/api?password=secret)", "[docs](https://h/api?password=Placeholder)")] | ||
| [InlineData("https://h/api?password=secret&uid=sa", "https://h/api?password=Placeholder&uid=sa")] | ||
| [InlineData("|Password=secret|Uid=sa|", "|Password=Placeholder|Uid=sa|")] | ||
| [InlineData("[Password=secret]", "[Password=Placeholder]")] | ||
| // A trailing sentence period is preserved, not consumed into the value. | ||
| [InlineData("The default is Password=secret.", "The default is Password=Placeholder.")] | ||
| public void RedactConnectionStringPasswords_StopsAtMarkdownAndUriDelimiters(string input, string expected) | ||
| { | ||
| Assert.Equal(expected, DocumentationSanitizer.RedactConnectionStringPasswords(input)); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.