georgepwall1991/ConfigContraband

GitHub: georgepwall1991/ConfigContraband

ConfigContraband是一款.NET配置验证工具,用于防止配置错误进入生产环境。

Stars: 1 | Forks: 0

ConfigContraband icon

# 配置违禁品 [![CI](https://static.pigsec.cn/wp-content/uploads/repos/2026/06/a01e0cde7d185159.svg)](https://github.com/georgepwall1991/ConfigContraband/actions/workflows/ci.yml) [![CodeQL](https://static.pigsec.cn/wp-content/uploads/repos/2026/06/4f09f797f8185201.svg)](https://github.com/georgepwall1991/ConfigContraband/actions/workflows/codeql.yml) [![codecov](https://codecov.io/gh/georgepwall1991/ConfigContraband/branch/main/graph/badge.svg)](https://codecov.io/gh/georgepwall1991/ConfigContraband) 停止将损坏的 `appsettings` 溜入生产环境。 ConfigContraband 是一个针对 .NET 配置、ASP.NET Core Options、`appsettings.json`、`ValidateOnStart()` 和 `ValidateDataAnnotations()` 的高信号 Roslyn 分析器。它捕捉到编译干净、通过代码审查,然后在启动时或更糟的是在首次使用时失败的配置错误。 它专注于无聊的生产故障: - `BindConfiguration(...)` 中的部分名称拼写错误 - 所有可见的 `appsettings*.json` 文件中缺少必需的配置密钥 - 存在但未在启动时运行的验证 - 永远未连接到 Options 验证的 `[Required]` 属性 - 看起来经过验证但被静默跳过的嵌套选项 - 隐藏在绑定部分下的拼写错误的 JSON 键 - 将引发异常的未知密钥的严格绑定 当您的应用程序依赖于强类型选项并且希望在编辑器、拉取请求和 CI 中获得配置验证反馈,在不良设置到达生产之前,请使用它。 ## 功能快照 | 区域 | ConfigContraband 做什么 | |------|----------------------------| | 部分绑定 | 检查受支持的选项绑定与可见的 `appsettings.json` 和 `appsettings.*.json` 文件。 | | 必需密钥 | 当 DataAnnotations 必需的密钥从所有可见配置文件中缺失时发出警告。 | | 启动验证 | 标记已注册但未强制在启动时运行的选项验证。 | | DataAnnotations | 找到 `[Required]`、`[Range]` 和继承的验证属性,而无需 `ValidateDataAnnotations()`。 | | 嵌套验证 | 检测需要递归验证属性的对象和集合。 | | JSON 键漂移 | 在保持灵活绑定形状保守的同时,报告绑定部分下可能拼写错误的键。 | | 严格绑定 | 当 `ErrorOnUnknownConfiguration` 使未知密钥成为绑定失败时发出警告。 | ## 安装 ``` ``` 该包包括 `buildTransitive` 属性,它自动将可见的 `appsettings.json` 和 `appsettings.*.json` 文件传递给分析器。添加包,构建,并让您的编辑器或 CI 告诉您您的选项合约和配置何时分离。 不会向您的应用程序添加运行时依赖项。ConfigContraband 在构建期间和受支持的 IDE 中作为分析器运行。 ## 它查看的内容 ConfigContraband 分析以下形状的选项注册: ``` services.AddOptions() .BindConfiguration("Stripe"); ``` 命名选项使用相同的受支持的 `OptionsBuilder` 形状: ``` services.AddOptions("tenant") .BindConfiguration("Stripe") .ValidateDataAnnotations() .ValidateOnStart(); ``` 它还识别常见的显式部分样式: ``` services.AddOptions() .Bind(configuration.GetSection("Stripe")); services.Configure( configuration.GetSection("Stripe")); ``` 分析器还遵循立即同一块局部 `OptionsBuilder` 链: ``` services.AddOptions() .BindConfiguration("Stripe") .ValidateDataAnnotations() .ValidateOnStart(); ``` 当绑定在声明分析器之后发生时,相同的跟踪也适用: ``` var optionsBuilder = services.AddOptions() .BindConfiguration("Stripe"); optionsBuilder.ValidateDataAnnotations(); optionsBuilder.ValidateOnStart(); ``` 在相同的局部构建器上相邻的验证调用可能出现在构建器初始化器中、绑定语句之前或之后。扫描在无关语句处停止,而不是猜测更宽的控制流。 当分析器无法在静态上证明配置形状时,它保持安静。目标是高信号反馈,而不是嘈杂的猜测。 ## 规则 | ID | 规则 | 默认 | 捕获 | |----|------|---------|---------| | `CFG001` | 绑定配置部分不存在 | 警告 | 只有 `Stripe` 存在时,`BindConfiguration("Strpie")`。 | | `CFG002` | 必需配置密钥缺失 | 警告 | 当启用 DataAnnotations 验证时,从所有可见的 `appsettings*.json` 部分中缺失 `[Required]` 引用、字符串或可空值属性。 | | `CFG003` | 选项验证未在启动时运行 | 警告 | 验证已注册但缺少 `ValidateOnStart()`。 | | `CFG004` | 选项验证未启用 DataAnnotations | 警告 | `[Required]`、`[Range]` 继承的属性或 `IValidatableObject` 没有使用 `ValidateDataAnnotations()`。 | | `CFG005` | 嵌套选项验证不是递归的 | 警告 | 嵌套对象或具有属性或 `IValidatableObject` 的项类型,但没有递归验证属性。 | | `CFG006` | 绑定部分下存在未知配置密钥 | 信息 | 不匹配可绑定选项属性或别名的 JSON 键。 | | `CFG007` | 绑定期间未知配置密钥将引发异常 | 警告 | 当启用 `ErrorOnUnknownConfiguration` 时,不匹配可绑定选项属性而 `ErrorOnUnknownConfiguration` 已启用。 | ## appsettings IntelliSense(模式生成) ConfigContraband 还可以反过来工作。它不仅可以事后标记 `appsettings.json` 错误,还可以从您的选项类型 **生成 JSON 模式**,这样您的编辑器就可以在您键入时提供自动完成、类型检查、必需密钥提示和未知密钥警告。 安装配套工具并生成模式: ``` dotnet tool install --global ConfigContraband.Tool configcontraband schema --project src/MyApp/MyApp.csproj ``` 它将 `appsettings.schema.json` 写入您的项目旁边。将设置文件指向它: ``` { "$schema": "appsettings.schema.json", "Stripe": { "ApiKey": "sk_live_..." } } ``` 现在 VS Code、Rider 和 Visual Studio 在您编辑 JSON 时实时提供: - **键完成**,对于每个绑定部分和属性,来自您的选项类。 - **类型检查**(字符串、数字、布尔值)和 **枚举值完成**。 - **必需字段提示**对于 `[Required]` 属性——与 `CFG002` 强制执行的合约相同。 - **来自 DataAnnotations 的值约束**。`[Range]` 成为 `minimum`/`maximum`(尊重 `MinimumIsExclusive`/`MaximumIsExclusive`),`[MaxLength]`/`[StringLength]` 成为 `maxLength`。因此,超出范围的端口或过长的值会在编辑器中标记为错误——与 `ValidateDataAnnotations()` 相同的失败,在键入时捕获,而不是在启动时。 - **悬停文档**。您的 `///` XML 文档注释——或 `[Description]`/`[DisplayName]`——在选项属性和类型上成为 JSON 模式 `description`,因此每个设置在悬停时都解释了自身。 - **JSON 中的未知密钥警告**。对于设置 `ErrorOnUnknownConfiguration = true` 的绑定,模式将部分标记为 `additionalProperties: false`,因此编辑器在应用程序启动之前标记了错误——`CFG007` 失败,在键入时捕获。 例如,这些选项: ``` public sealed class ServerOptions { /// TCP port the server listens on. [Range(1, 65535)] public int Port { get; set; } /// API key used to authenticate outbound calls. [StringLength(64)] public string ApiKey { get; set; } = ""; } ``` 生成此模式片段,因此编辑器强制执行范围和最大长度,并在悬停时显示每个设置的文档: ``` "Port": { "type": "integer", "description": "TCP port the server listens on.", "minimum": 1, "maximum": 65535 }, "ApiKey": { "type": "string", "description": "API key used to authenticate outbound calls.", "maxLength": 64 } ``` 生成器重用了分析器相同的可绑定属性模型,包括 `[ConfigurationKeyName]` 别名、嵌套对象、集合和字典。每个发出的约束都反映了 `Microsoft.Extensions.Options` 验证实际执行的约束,并且设计上保守:仅针对调用 `ValidateDataAnnotations()` 的绑定编写约束(因此宽松的配置永远不会过度约束),生成器永远不会发出运行时绑定器接受的值可以拒绝的约束,并且宽松的绑定保持开放(`additionalProperties` 未设置),因此灵活的配置仍然有效。因此,一些属性有意留作未约束:`[RegularExpression]`(.NET 正则表达式与 JSON 模式的 ECMA-262 `pattern` 不同)、`[EmailAddress]`/`[Url]`(严格的 `format` 语法比属性的宽松检查严格)、`[MinLength]`(JSON 模式计算 Unicode 代码点,而 DataAnnotations 计算 UTF-16 单位)——否则可能会标记运行时接受的配置。 使用 `--check` 在 CI 中保持提交的模式诚实,该命令在内存中重新生成并在模式过时时退出非零: ``` configcontraband schema --project src/MyApp/MyApp.csproj --check ``` ## 快速反馈循环 存储库包含一个展示项目,其中每个规则都有一个故意的示例: ``` dotnet build samples/ConfigContraband.Showcase/ConfigContraband.Showcase.csproj --configuration Release --no-incremental ``` 示例保持在与主解决方案分开,以便正常的开发构建保持干净。 ## 规则细节 ### `CFG001`:部分必须存在 如果您的代码绑定 `"Stripe"`,可见的 `appsettings.json` 或 `appsettings.*.json` 文件应包含匹配的 `Stripe` 部分。 在之前: ``` services.AddOptions() .BindConfiguration("Strpie") .ValidateDataAnnotations() .ValidateOnStart(); ``` ``` { "Stripe": { "ApiKey": "secret" } } ``` 在之后: ``` services.AddOptions() .BindConfiguration("Stripe") .ValidateDataAnnotations() .ValidateOnStart(); ``` 当 ConfigContraband 看到可能的拼写错误时,它可以提供代码修复。修复在替换部分名称时保持常规、逐字和原始字符串字面量样式,如果需要换行则回退到转义字符串字面量。嵌套部分路径使用与 .NET 配置相同的冒号分隔形状: ``` services.AddOptions() .BindConfiguration("Features:Stripe") .ValidateDataAnnotations() .ValidateOnStart(); ``` ``` { "Features": { "Stripe": { "ApiKey": "secret" } } } ``` 对于嵌套拼写错误,修复保持父路径并仅替换错误的叶部分。如果代码说 `Features:Strpie` 而文件包含 `Features:Stripe`,则修复将其更改为 `Features:Stripe`。 分析器检查每个可见的 `appsettings.json` 和 `appsettings.*.json` 额外文件是否存在部分,包括注释文件、JSON 字符串转义、冒号分隔的键(例如 `"Features:Stripe"`)以及在解析嵌套部分路径时重复的 JSON 部分成员。类似 `appsettingsBackup.json` 的文件被忽略。当没有可用的 appsettings 文件时,它保持安静,因为它无法证明在运行时存在什么配置。 ### `CFG002`:必需配置密钥必须存在 `CFG002` 在支持绑定具有可见的 DataAnnotations 验证路径时运行。这包括带有 `ValidateDataAnnotations()` 的 `OptionsBuilder` 链和直接 `Configure(GetSection(...))` 调用,当相同的顶级块也注册匹配的 `AddOptions().ValidateDataAnnotations()` 时。它报告 `[Required]` 引用、字符串或可空值属性,这些属性缺失在为该绑定提供的每个可见的 `appsettings.json` 和 `appsettings.*.json` 部分中。 在之前: ``` public sealed class StripeOptions { [Required] public string ApiKey { get; set; } = ""; } services.AddOptions() .BindConfiguration("Stripe") .ValidateDataAnnotations() .ValidateOnStart(); ``` ``` { "Stripe": { } } ``` 在之后: ``` { "Stripe": { "ApiKey": "secret" } } ``` 该规则遵循与 Options 验证相同的运行时验证边界。C# `required` 成员是编译时对象初始化器检查,而不是 DataAnnotations 验证,因此不会报告。`[Required]` 在非可空值类型上也被忽略,因为默认值不是 null。嵌套对象和集合项仅在递归验证属性使 Options 验证遍历这些值时进行检查;字典值对象保持安静。 ### `CFG003`:验证应在应用程序启动时运行 选项验证通常在首次使用选项时运行。`ValidateOnStart()` 将该失败移动到启动时,这是它应该出现的地方。 在之前: ``` services.AddOptions() .BindConfiguration("Stripe") .ValidateDataAnnotations(); ``` 在之后: ``` services.AddOptions() .BindConfiguration("Stripe") .ValidateDataAnnotations() .ValidateOnStart(); ``` 分析器跟踪同一流畅链上的验证调用,无论它们出现在绑定调用之前还是之后。代码修复以与现有注册链相同的风格追加 `ValidateOnStart()`,包括多行链和绑定发生初始化器或后续局部语句的立即同一块局部 `OptionsBuilder` 链。对于后续局部绑定语句,同一局部上的相邻验证调用从构建器初始化器、绑定之前和绑定之后识别。以 `AddOptionsWithValidateOnStart()` 开头的注册已经运行验证,因此 `CFG003` 对该形状保持安静。 `CFG003` 仅将框架 `OptionsBuilder.Validate(...)`、`ValidateDataAnnotations()` 和 `ValidateOnStart()` API 视为验证信号。具有相同名称的自定义扩展方法被忽略,除非它们以分析器可以查看的方式调用框架 API。 ### `CFG004`:必须启用 DataAnnotations 如 `[Required]` 之类的属性,除非注册 `ValidateDataAnnotations()`,否则对选项验证不起作用。继承的可绑定属性也计入,包括通过派生构造函数填充的继承的只读属性,因此具有 DataAnnotations 的基选项类仍然需要在派生选项注册上启用验证。类型级别的验证属性也包括在内,因为 DataAnnotations 在选项对象本身上评估 `ValidationAttribute`。嵌套选项图也计入:如果嵌套类或列表样式集合项具有属性级或类型级验证属性,或在其可绑定对象图中任何地方实现 `IValidatableObject`,则标记每个应递归检查的父属性。初始化的只读对象或可变集合属性计入,因为配置绑定器可以填充它们的现有实例。公共私有设置的嵌套属性也计入,当绑定调用在运行时绑定器支持的单一公共参数化构造函数上显式设置 `BindNonPublicProperties = true` 时。如果属性绑定选项使用 `[ConfigurationKeyName]`,则配置的名称替换 CLR 属性名称以进行匹配。构造函数绑定属性使用构造函数参数键,匹配运行时绑定器;如果属性在构造之后也可设置,包括由 `BindNonPublicProperties` 启用的私有设置器,则接受 `[ConfigurationKeyName]` 别名,当构造函数键存在或构造函数参数具有默认值时。在匹配之前解码 JSON 字符串转义,因此转义属性名称与它们的运行时配置密钥处理相同。 在之前: ``` public class BillingOptions { [Required] public string ApiKey { get; set; } = ""; } public sealed class StripeOptions : BillingOptions { public string WebhookSecret { get; set; } = ""; } services.AddOptions() .BindConfiguration("Stripe") .ValidateOnStart(); ``` 在之后: ``` services.AddOptions() .BindConfiguration("Stripe") .ValidateDataAnnotations() .ValidateOnStart(); ``` `Validate(...)` 计算为 `CFG3` 的验证,但不满足 `CFG004` 当存在 DataAnnotations 属性时。 分析器识别同一流畅链上的 `ValidateDataAnnotations()`,无论它们出现在绑定调用之前还是之后。代码修复保留现有的流畅链格式,添加 `ValidateDataAnnotations()`,并且仅在启动验证尚未存在时添加 `ValidateOnStart()`,包括以 `AddOptionsWithValidateOnStart()` 开头的注册。 与 `CFG003` 一样,`CFG004` 符号检查框架验证扩展方法。名为 `ValidateDataAnnotations(...)` 的项目本地辅助程序仅按名称不满足规则。 ### `CFG005`:嵌套选项需要递归验证 DataAnnotations 不会自动进入子对象或集合项。如果嵌套类或列表样式集合项具有属性级或类型级验证属性,或在其可绑定对象图中任何地方实现 `IValidatableObject`,则标记每个应递归检查的父属性。初始化的只读对象和可变集合属性计入,因为配置绑定器可以填充它们的现有实例。公共私有设置的嵌套属性也计入,当绑定调用在运行时绑定器支持的单一公共参数化构造函数上显式设置 `BindNonPublicProperties = true` 时。 在之前: ``` public sealed class AppOptions { public DatabaseOptions Database { get; set; } = new(); } public sealed class DatabaseOptions { [Required] public string ConnectionString { get; set; } = ""; } ``` 在之后: ``` using Microsoft.Extensions.Options; public sealed class AppOptions { [ValidateObjectMembers] public DatabaseOptions Database { get; set; } = new(); } public sealed class DatabaseOptions { [Required] public string ConnectionString { get; set; } = ""; } ``` 对于数组和其他 `IEnumerable` 选项集合,使用 `[ValidateEnumeratedItems]`。当存在恰好一个公共参数
标签:appsettings.json, ASP.NET Core, Homebrew安装, JSON解析, Roslyn, 代码分析, 代码审查, 凭证管理, 图计算, 多人体追踪, 开源框架, 持续集成, 数据验证, 生产环境安全, 配置错误检测, 配置项绑定, 配置验证