From e194f53fb750c1d8c41bf1527a51c3a4f532a42c Mon Sep 17 00:00:00 2001 From: AG Date: Sun, 11 Oct 2026 09:21:11 +0700 Subject: [PATCH 01/12] ci: enforce C# style rules and add SPDX file headers Add .editorconfig, a CI step running dotnet format style --verify-no-changes --severity warn, SPDX copyright/license headers on every C# file, and remove unused using directives. No behavior change. --- .editorconfig | 42 +++++++++++++++++++ .github/workflows/ci.yml | 2 + src/PostgreSqlMcp.Core/DatabaseLockCheck.cs | 3 ++ src/PostgreSqlMcp.Core/DatabaseRegistry.cs | 3 ++ src/PostgreSqlMcp.Core/QueryPage.cs | 3 ++ src/PostgreSqlMcp.Core/SecurityWarnings.cs | 3 ++ src/PostgreSqlMcp.Core/ServerOptions.cs | 3 ++ src/PostgreSqlMcp.Core/SqlExecutor.cs | 3 ++ src/PostgreSqlMcp.Core/SqlGuard.cs | 3 ++ src/PostgreSqlMcp.Core/SqlParameters.cs | 3 ++ src/PostgreSqlMcp.Core/ToolReply.cs | 3 ++ src/PostgreSqlMcp.Tools/DatabaseTools.cs | 3 ++ src/PostgreSqlMcp.Tools/OpsTools.cs | 3 ++ src/PostgreSqlMcp.Tools/ParameterText.cs | 3 ++ src/PostgreSqlMcp.Tools/PlanTools.cs | 3 ++ src/PostgreSqlMcp.Tools/SqlTools.cs | 3 ++ src/PostgreSqlMcp.Tools/ToolRegistration.cs | 3 ++ src/PostgreSqlMcp/Program.cs | 4 +- tools/PostgreSqlMcp.Build/Program.cs | 3 ++ tools/PostgreSqlMcp.Fuzz/FuzzCampaign.cs | 4 +- tools/PostgreSqlMcp.Fuzz/Program.cs | 3 ++ tools/PostgreSqlMcp.NpmInstall/Program.cs | 4 +- tools/PostgreSqlMcp.Release/NuGetRegistry.cs | 3 ++ tools/PostgreSqlMcp.Release/Program.cs | 3 ++ tools/PostgreSqlMcp.Release/ReleaseArchive.cs | 3 ++ tools/PostgreSqlMcp.Release/ReleaseAssets.cs | 3 ++ .../DatabaseLockChecks.cs | 3 ++ tools/PostgreSqlMcp.Verify/FuzzChecks.cs | 3 ++ tools/PostgreSqlMcp.Verify/Integration.cs | 4 +- tools/PostgreSqlMcp.Verify/McpClient.cs | 3 ++ tools/PostgreSqlMcp.Verify/NpmRegistry.cs | 3 ++ tools/PostgreSqlMcp.Verify/Packages.cs | 3 ++ tools/PostgreSqlMcp.Verify/PostgresFixture.cs | 3 ++ tools/PostgreSqlMcp.Verify/Program.cs | 3 ++ .../ReleaseRegressions.cs | 3 ++ tools/PostgreSqlMcp.Verify/ResourceChecks.cs | 3 ++ tools/PostgreSqlMcp.Verify/SarifGate.cs | 3 ++ tools/PostgreSqlMcp.Verify/SarifRegression.cs | 3 ++ tools/Shared/NpmPlatforms.cs | 3 ++ tools/Shared/ProcessRunner.cs | 3 ++ tools/Shared/RepositoryRoot.cs | 3 ++ 41 files changed, 161 insertions(+), 4 deletions(-) create mode 100644 .editorconfig diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..e25eacc --- /dev/null +++ b/.editorconfig @@ -0,0 +1,42 @@ +# Repository-wide formatting and the C# style rules that CI enforces. +# `dotnet format style postgresql-sharp-mcp.slnx --verify-no-changes --no-restore --severity warn` +# fails on any warning below; see CONTRIBUTING.md#coding-standards. +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 4 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{yml,yaml,json,csproj,props,slnx,md}] +indent_size = 2 + +[*.cs] +# Using directives: no unused ones, outside the namespace. +dotnet_diagnostic.IDE0005.severity = warning +csharp_using_directive_placement = outside_namespace:warning +# File-scoped namespaces. +csharp_style_namespace_declarations = file_scoped:warning +# Explicit accessibility on every non-interface member. +dotnet_style_require_accessibility_modifiers = for_non_interface_members:warning + +# Naming (.NET Framework Design Guidelines): PascalCase types and members, I-prefixed interfaces. +dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum, delegate +dotnet_naming_symbols.members.applicable_kinds = method, property, event +dotnet_naming_symbols.members.applicable_accessibilities = public, internal, protected, protected_internal +dotnet_naming_symbols.interfaces.applicable_kinds = interface +dotnet_naming_style.pascal_case.capitalization = pascal_case +dotnet_naming_style.i_prefix.capitalization = pascal_case +dotnet_naming_style.i_prefix.required_prefix = I +dotnet_naming_rule.interfaces_i_prefix.symbols = interfaces +dotnet_naming_rule.interfaces_i_prefix.style = i_prefix +dotnet_naming_rule.interfaces_i_prefix.severity = warning +dotnet_naming_rule.types_pascal.symbols = types +dotnet_naming_rule.types_pascal.style = pascal_case +dotnet_naming_rule.types_pascal.severity = warning +dotnet_naming_rule.members_pascal.symbols = members +dotnet_naming_rule.members_pascal.style = pascal_case +dotnet_naming_rule.members_pascal.severity = warning diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3254712..404a156 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,6 +26,8 @@ jobs: node-version: '24' - name: Restore locked dependencies run: dotnet restore postgresql-sharp-mcp.slnx --locked-mode + - name: Enforce C# coding style (.editorconfig) + run: dotnet format style postgresql-sharp-mcp.slnx --verify-no-changes --no-restore --severity warn - name: Real PostgreSQL MCP integration run: dotnet run --project tools/PostgreSqlMcp.Verify -c Release -- integration - name: Build npm and NuGet packages with .NET diff --git a/src/PostgreSqlMcp.Core/DatabaseLockCheck.cs b/src/PostgreSqlMcp.Core/DatabaseLockCheck.cs index 08be00f..938779b 100644 --- a/src/PostgreSqlMcp.Core/DatabaseLockCheck.cs +++ b/src/PostgreSqlMcp.Core/DatabaseLockCheck.cs @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 codegiveness +// SPDX-License-Identifier: MIT + using Npgsql; namespace PostgreSqlMcp.Core; diff --git a/src/PostgreSqlMcp.Core/DatabaseRegistry.cs b/src/PostgreSqlMcp.Core/DatabaseRegistry.cs index 58e7d2e..a73e436 100644 --- a/src/PostgreSqlMcp.Core/DatabaseRegistry.cs +++ b/src/PostgreSqlMcp.Core/DatabaseRegistry.cs @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 codegiveness +// SPDX-License-Identifier: MIT + using System.Text; using Npgsql; diff --git a/src/PostgreSqlMcp.Core/QueryPage.cs b/src/PostgreSqlMcp.Core/QueryPage.cs index 939be93..a27048d 100644 --- a/src/PostgreSqlMcp.Core/QueryPage.cs +++ b/src/PostgreSqlMcp.Core/QueryPage.cs @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 codegiveness +// SPDX-License-Identifier: MIT + namespace PostgreSqlMcp.Core; public sealed record ColumnInfo(string Name, string Type); diff --git a/src/PostgreSqlMcp.Core/SecurityWarnings.cs b/src/PostgreSqlMcp.Core/SecurityWarnings.cs index cf5a2f4..3173aa1 100644 --- a/src/PostgreSqlMcp.Core/SecurityWarnings.cs +++ b/src/PostgreSqlMcp.Core/SecurityWarnings.cs @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 codegiveness +// SPDX-License-Identifier: MIT + using System.Net; using Npgsql; diff --git a/src/PostgreSqlMcp.Core/ServerOptions.cs b/src/PostgreSqlMcp.Core/ServerOptions.cs index b894b99..ed39d6e 100644 --- a/src/PostgreSqlMcp.Core/ServerOptions.cs +++ b/src/PostgreSqlMcp.Core/ServerOptions.cs @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 codegiveness +// SPDX-License-Identifier: MIT + using System.Collections.ObjectModel; using System.Globalization; using System.Text.Json; diff --git a/src/PostgreSqlMcp.Core/SqlExecutor.cs b/src/PostgreSqlMcp.Core/SqlExecutor.cs index 336a016..e48aa23 100644 --- a/src/PostgreSqlMcp.Core/SqlExecutor.cs +++ b/src/PostgreSqlMcp.Core/SqlExecutor.cs @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 codegiveness +// SPDX-License-Identifier: MIT + using System.Buffers; using System.Data; using System.Text.Json; diff --git a/src/PostgreSqlMcp.Core/SqlGuard.cs b/src/PostgreSqlMcp.Core/SqlGuard.cs index 82df89b..4c814c4 100644 --- a/src/PostgreSqlMcp.Core/SqlGuard.cs +++ b/src/PostgreSqlMcp.Core/SqlGuard.cs @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 codegiveness +// SPDX-License-Identifier: MIT + namespace PostgreSqlMcp.Core; // This is a statement-boundary lexer, not a SQL authorization parser. PostgreSQL diff --git a/src/PostgreSqlMcp.Core/SqlParameters.cs b/src/PostgreSqlMcp.Core/SqlParameters.cs index 3f39774..e692d22 100644 --- a/src/PostgreSqlMcp.Core/SqlParameters.cs +++ b/src/PostgreSqlMcp.Core/SqlParameters.cs @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 codegiveness +// SPDX-License-Identifier: MIT + using System.Text; using System.Text.Json; using Npgsql; diff --git a/src/PostgreSqlMcp.Core/ToolReply.cs b/src/PostgreSqlMcp.Core/ToolReply.cs index 708f3b7..e94321e 100644 --- a/src/PostgreSqlMcp.Core/ToolReply.cs +++ b/src/PostgreSqlMcp.Core/ToolReply.cs @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 codegiveness +// SPDX-License-Identifier: MIT + using System.Text; using System.Text.Json; using System.Text.Json.Serialization; diff --git a/src/PostgreSqlMcp.Tools/DatabaseTools.cs b/src/PostgreSqlMcp.Tools/DatabaseTools.cs index 38710f7..d3ab1a4 100644 --- a/src/PostgreSqlMcp.Tools/DatabaseTools.cs +++ b/src/PostgreSqlMcp.Tools/DatabaseTools.cs @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 codegiveness +// SPDX-License-Identifier: MIT + using System.ComponentModel; using System.Globalization; using ModelContextProtocol.Protocol; diff --git a/src/PostgreSqlMcp.Tools/OpsTools.cs b/src/PostgreSqlMcp.Tools/OpsTools.cs index c631e63..c43381c 100644 --- a/src/PostgreSqlMcp.Tools/OpsTools.cs +++ b/src/PostgreSqlMcp.Tools/OpsTools.cs @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 codegiveness +// SPDX-License-Identifier: MIT + using System.ComponentModel; using ModelContextProtocol.Protocol; using ModelContextProtocol.Server; diff --git a/src/PostgreSqlMcp.Tools/ParameterText.cs b/src/PostgreSqlMcp.Tools/ParameterText.cs index 229f764..ee2a7fe 100644 --- a/src/PostgreSqlMcp.Tools/ParameterText.cs +++ b/src/PostgreSqlMcp.Tools/ParameterText.cs @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 codegiveness +// SPDX-License-Identifier: MIT + namespace PostgreSqlMcp.Tools; /// Parameter descriptions shared by several tools; one wording per concept, kept short because every tools/list repeats them. diff --git a/src/PostgreSqlMcp.Tools/PlanTools.cs b/src/PostgreSqlMcp.Tools/PlanTools.cs index b1e699a..31e5a01 100644 --- a/src/PostgreSqlMcp.Tools/PlanTools.cs +++ b/src/PostgreSqlMcp.Tools/PlanTools.cs @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 codegiveness +// SPDX-License-Identifier: MIT + using System.Buffers; using System.ComponentModel; using System.Data; diff --git a/src/PostgreSqlMcp.Tools/SqlTools.cs b/src/PostgreSqlMcp.Tools/SqlTools.cs index acaad64..262c288 100644 --- a/src/PostgreSqlMcp.Tools/SqlTools.cs +++ b/src/PostgreSqlMcp.Tools/SqlTools.cs @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 codegiveness +// SPDX-License-Identifier: MIT + using System.ComponentModel; using System.Text.Json; using ModelContextProtocol.Protocol; diff --git a/src/PostgreSqlMcp.Tools/ToolRegistration.cs b/src/PostgreSqlMcp.Tools/ToolRegistration.cs index b0d2769..9ec8a6c 100644 --- a/src/PostgreSqlMcp.Tools/ToolRegistration.cs +++ b/src/PostgreSqlMcp.Tools/ToolRegistration.cs @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 codegiveness +// SPDX-License-Identifier: MIT + using System.ComponentModel; using System.Reflection; using Microsoft.Extensions.DependencyInjection; diff --git a/src/PostgreSqlMcp/Program.cs b/src/PostgreSqlMcp/Program.cs index 1ab7ccf..7b9efea 100644 --- a/src/PostgreSqlMcp/Program.cs +++ b/src/PostgreSqlMcp/Program.cs @@ -1,8 +1,10 @@ +// SPDX-FileCopyrightText: 2026 codegiveness +// SPDX-License-Identifier: MIT + using System.Reflection; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; -using ModelContextProtocol.Server; using PostgreSqlMcp.Core; using PostgreSqlMcp.Tools; diff --git a/tools/PostgreSqlMcp.Build/Program.cs b/tools/PostgreSqlMcp.Build/Program.cs index b38b7a0..14647f9 100644 --- a/tools/PostgreSqlMcp.Build/Program.cs +++ b/tools/PostgreSqlMcp.Build/Program.cs @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 codegiveness +// SPDX-License-Identifier: MIT + using System.Formats.Tar; using System.IO.Compression; using System.Text.Json; diff --git a/tools/PostgreSqlMcp.Fuzz/FuzzCampaign.cs b/tools/PostgreSqlMcp.Fuzz/FuzzCampaign.cs index 5ec3984..6597d8d 100644 --- a/tools/PostgreSqlMcp.Fuzz/FuzzCampaign.cs +++ b/tools/PostgreSqlMcp.Fuzz/FuzzCampaign.cs @@ -1,4 +1,6 @@ -using System.Diagnostics; +// SPDX-FileCopyrightText: 2026 codegiveness +// SPDX-License-Identifier: MIT + using System.Globalization; using System.Runtime.InteropServices; using System.Security.Cryptography; diff --git a/tools/PostgreSqlMcp.Fuzz/Program.cs b/tools/PostgreSqlMcp.Fuzz/Program.cs index d3fdd92..cb27f7e 100644 --- a/tools/PostgreSqlMcp.Fuzz/Program.cs +++ b/tools/PostgreSqlMcp.Fuzz/Program.cs @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 codegiveness +// SPDX-License-Identifier: MIT + using System.Text; using PostgreSqlMcp.Core; using SharpFuzz; diff --git a/tools/PostgreSqlMcp.NpmInstall/Program.cs b/tools/PostgreSqlMcp.NpmInstall/Program.cs index 68f3e05..067d989 100644 --- a/tools/PostgreSqlMcp.NpmInstall/Program.cs +++ b/tools/PostgreSqlMcp.NpmInstall/Program.cs @@ -1,4 +1,6 @@ -using System.Diagnostics; +// SPDX-FileCopyrightText: 2026 codegiveness +// SPDX-License-Identifier: MIT + using System.Runtime.InteropServices; try diff --git a/tools/PostgreSqlMcp.Release/NuGetRegistry.cs b/tools/PostgreSqlMcp.Release/NuGetRegistry.cs index 123419c..11306b9 100644 --- a/tools/PostgreSqlMcp.Release/NuGetRegistry.cs +++ b/tools/PostgreSqlMcp.Release/NuGetRegistry.cs @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 codegiveness +// SPDX-License-Identifier: MIT + using System.IO.Compression; using System.Net; using System.Security.Cryptography; diff --git a/tools/PostgreSqlMcp.Release/Program.cs b/tools/PostgreSqlMcp.Release/Program.cs index 2307a87..b929e2e 100644 --- a/tools/PostgreSqlMcp.Release/Program.cs +++ b/tools/PostgreSqlMcp.Release/Program.cs @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 codegiveness +// SPDX-License-Identifier: MIT + using System.Collections; using System.Security.Cryptography; using System.Text.Json; diff --git a/tools/PostgreSqlMcp.Release/ReleaseArchive.cs b/tools/PostgreSqlMcp.Release/ReleaseArchive.cs index ae17302..2336666 100644 --- a/tools/PostgreSqlMcp.Release/ReleaseArchive.cs +++ b/tools/PostgreSqlMcp.Release/ReleaseArchive.cs @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 codegiveness +// SPDX-License-Identifier: MIT + using System.Formats.Tar; using System.IO.Compression; diff --git a/tools/PostgreSqlMcp.Release/ReleaseAssets.cs b/tools/PostgreSqlMcp.Release/ReleaseAssets.cs index 9b80848..9fd0459 100644 --- a/tools/PostgreSqlMcp.Release/ReleaseAssets.cs +++ b/tools/PostgreSqlMcp.Release/ReleaseAssets.cs @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 codegiveness +// SPDX-License-Identifier: MIT + using System.Text.Json; /// One asset as reported by the GitHub Releases API. diff --git a/tools/PostgreSqlMcp.Verify/DatabaseLockChecks.cs b/tools/PostgreSqlMcp.Verify/DatabaseLockChecks.cs index dd28d23..daf3a97 100644 --- a/tools/PostgreSqlMcp.Verify/DatabaseLockChecks.cs +++ b/tools/PostgreSqlMcp.Verify/DatabaseLockChecks.cs @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 codegiveness +// SPDX-License-Identifier: MIT + using System.Text.Json; using System.Text.Json.Nodes; diff --git a/tools/PostgreSqlMcp.Verify/FuzzChecks.cs b/tools/PostgreSqlMcp.Verify/FuzzChecks.cs index 9fd1926..1a0bd4d 100644 --- a/tools/PostgreSqlMcp.Verify/FuzzChecks.cs +++ b/tools/PostgreSqlMcp.Verify/FuzzChecks.cs @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 codegiveness +// SPDX-License-Identifier: MIT + using System.Text; using System.Text.Json; using FsCheck; diff --git a/tools/PostgreSqlMcp.Verify/Integration.cs b/tools/PostgreSqlMcp.Verify/Integration.cs index 54caeaa..5333f4d 100644 --- a/tools/PostgreSqlMcp.Verify/Integration.cs +++ b/tools/PostgreSqlMcp.Verify/Integration.cs @@ -1,4 +1,6 @@ -using System.Text; +// SPDX-FileCopyrightText: 2026 codegiveness +// SPDX-License-Identifier: MIT + using System.Text.Json; using System.Text.Json.Nodes; diff --git a/tools/PostgreSqlMcp.Verify/McpClient.cs b/tools/PostgreSqlMcp.Verify/McpClient.cs index f6e26f0..a7cc46a 100644 --- a/tools/PostgreSqlMcp.Verify/McpClient.cs +++ b/tools/PostgreSqlMcp.Verify/McpClient.cs @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 codegiveness +// SPDX-License-Identifier: MIT + using System.Collections.Concurrent; using System.Diagnostics; using System.Text; diff --git a/tools/PostgreSqlMcp.Verify/NpmRegistry.cs b/tools/PostgreSqlMcp.Verify/NpmRegistry.cs index 4cca939..ca63229 100644 --- a/tools/PostgreSqlMcp.Verify/NpmRegistry.cs +++ b/tools/PostgreSqlMcp.Verify/NpmRegistry.cs @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 codegiveness +// SPDX-License-Identifier: MIT + using System.Formats.Tar; using System.IO.Compression; using System.Net; diff --git a/tools/PostgreSqlMcp.Verify/Packages.cs b/tools/PostgreSqlMcp.Verify/Packages.cs index 05421ae..1282c1d 100644 --- a/tools/PostgreSqlMcp.Verify/Packages.cs +++ b/tools/PostgreSqlMcp.Verify/Packages.cs @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 codegiveness +// SPDX-License-Identifier: MIT + using System.Data.Common; using System.Formats.Tar; using System.IO.Compression; diff --git a/tools/PostgreSqlMcp.Verify/PostgresFixture.cs b/tools/PostgreSqlMcp.Verify/PostgresFixture.cs index ae06789..7a36246 100644 --- a/tools/PostgreSqlMcp.Verify/PostgresFixture.cs +++ b/tools/PostgreSqlMcp.Verify/PostgresFixture.cs @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 codegiveness +// SPDX-License-Identifier: MIT + using System.Globalization; namespace PostgreSqlMcp.Verify; diff --git a/tools/PostgreSqlMcp.Verify/Program.cs b/tools/PostgreSqlMcp.Verify/Program.cs index 6778c39..56c2888 100644 --- a/tools/PostgreSqlMcp.Verify/Program.cs +++ b/tools/PostgreSqlMcp.Verify/Program.cs @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 codegiveness +// SPDX-License-Identifier: MIT + using System.Diagnostics; using System.Runtime.InteropServices; using System.Text.Json; diff --git a/tools/PostgreSqlMcp.Verify/ReleaseRegressions.cs b/tools/PostgreSqlMcp.Verify/ReleaseRegressions.cs index bca80ff..2190586 100644 --- a/tools/PostgreSqlMcp.Verify/ReleaseRegressions.cs +++ b/tools/PostgreSqlMcp.Verify/ReleaseRegressions.cs @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 codegiveness +// SPDX-License-Identifier: MIT + using System.Formats.Tar; using System.IO.Compression; using System.Security.Cryptography; diff --git a/tools/PostgreSqlMcp.Verify/ResourceChecks.cs b/tools/PostgreSqlMcp.Verify/ResourceChecks.cs index adafc2e..9f6a03c 100644 --- a/tools/PostgreSqlMcp.Verify/ResourceChecks.cs +++ b/tools/PostgreSqlMcp.Verify/ResourceChecks.cs @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 codegiveness +// SPDX-License-Identifier: MIT + using System.Diagnostics; using System.Globalization; diff --git a/tools/PostgreSqlMcp.Verify/SarifGate.cs b/tools/PostgreSqlMcp.Verify/SarifGate.cs index 7882e14..5b0192d 100644 --- a/tools/PostgreSqlMcp.Verify/SarifGate.cs +++ b/tools/PostgreSqlMcp.Verify/SarifGate.cs @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 codegiveness +// SPDX-License-Identifier: MIT + using System.Globalization; using System.Text; using System.Text.Json; diff --git a/tools/PostgreSqlMcp.Verify/SarifRegression.cs b/tools/PostgreSqlMcp.Verify/SarifRegression.cs index 29bd3fd..d0bf2a7 100644 --- a/tools/PostgreSqlMcp.Verify/SarifRegression.cs +++ b/tools/PostgreSqlMcp.Verify/SarifRegression.cs @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 codegiveness +// SPDX-License-Identifier: MIT + using System.Text.Json; using System.Text.Json.Nodes; diff --git a/tools/Shared/NpmPlatforms.cs b/tools/Shared/NpmPlatforms.cs index 72d28ea..11d2266 100644 --- a/tools/Shared/NpmPlatforms.cs +++ b/tools/Shared/NpmPlatforms.cs @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 codegiveness +// SPDX-License-Identifier: MIT + /// /// One npm platform package per supported runtime identifier. The main npm package lists every one in /// optionalDependencies; npm installs only the package whose os/cpu/libc fields match the machine. diff --git a/tools/Shared/ProcessRunner.cs b/tools/Shared/ProcessRunner.cs index e47f72c..b3f0567 100644 --- a/tools/Shared/ProcessRunner.cs +++ b/tools/Shared/ProcessRunner.cs @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 codegiveness +// SPDX-License-Identifier: MIT + using System.Diagnostics; // Shared by the tool projects through ; this file is the only home of diff --git a/tools/Shared/RepositoryRoot.cs b/tools/Shared/RepositoryRoot.cs index e5589f0..a7d5339 100644 --- a/tools/Shared/RepositoryRoot.cs +++ b/tools/Shared/RepositoryRoot.cs @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2026 codegiveness +// SPDX-License-Identifier: MIT + internal static class RepositoryRoot { /// The nearest ancestor of the current directory (inclusive) containing the solution file, or null outside the repository. From 257a93401efdfb78e7900612e4fde11a72be23a9 Mon Sep 17 00:00:00 2001 From: AG Date: Sun, 11 Oct 2026 09:21:11 +0700 Subject: [PATCH 02/12] docs: add governance, architecture, roadmap, assurance case and review standards Agent-authored. Documents the sole-maintainer governance and continuity limits, coding and code-review standards, vulnerability handling, and a security assurance case with residual risks. --- CHANGELOG.md | 7 ++ CONTRIBUTING.md | 48 +++++++++++++ GOVERNANCE.md | 82 +++++++++++++++++++++ README.md | 2 + SECURITY.md | 15 ++++ docs/architecture.md | 92 ++++++++++++++++++++++++ docs/assurance-case.md | 158 +++++++++++++++++++++++++++++++++++++++++ docs/roadmap.md | 40 +++++++++++ 8 files changed, 444 insertions(+) create mode 100644 GOVERNANCE.md create mode 100644 docs/architecture.md create mode 100644 docs/assurance-case.md create mode 100644 docs/roadmap.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ccabeb..91c4342 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ ## Unreleased +Documentation, policy and tooling only; no runtime behavior change. + +- Add [GOVERNANCE.md](GOVERNANCE.md) (decision-making, roles and what happens if the sole maintainer is unavailable; it states that `access_continuity` is not met), [docs/architecture.md](docs/architecture.md), [docs/roadmap.md](docs/roadmap.md) and [docs/assurance-case.md](docs/assurance-case.md) (requirements, threat model, trust boundaries, secure-design principles, common weaknesses and residual risks; the review record is pending the maintainer). +- CONTRIBUTING: add coding standards, code-review standards and small-task guidance. Add an `.editorconfig` and a CI step (`dotnet format style --verify-no-changes --severity warn`) that enforces the C# style rules. Source files gain SPDX copyright and license headers, and unused `using` directives are removed. +- SECURITY: document how vulnerability reports are handled. +- Propose Silver and Gold Best Practices answers in `.bestpractices.json`, and record the live Scorecard figures, the reference project's figures, the criteria that remain unmet and an owner checklist in [docs/security-posture.md](docs/security-posture.md). Nothing is saved on bestpractices.dev until the owner reviews it. + ## 0.5.0 Upgrade notes: a locked configuration whose login can execute server-file functions is now refused at startup, and npm installs that omit optional dependencies now fail (both below). Existing tool names, parameters, environment variables, CLI flags and the targets-file format remain compatible; additions such as the `parameters` input of `execute_sql`/`explain_query` are optional. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2eff8d8..43e31cb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -192,6 +192,31 @@ docker run --rm -i \ The database hostname must be reachable from the container; `localhost` refers to the container itself. Retain certificate validation for remote connections and mount any required CA material at the connection string's container path. For the optional credentials file, ensure the non-root runtime user can read it using a suitable group/UID or secret mount, not world-readable permissions. `-i` keeps stdin available for MCP stdio. Both routes use one bootstrap profile to select live physical databases without per-database JSON edits. +## Coding standards + +All maintained code is C# on .NET 10 with the SDK-default language version. The style guides are Microsoft's [C# coding conventions](https://learn.microsoft.com/dotnet/csharp/fundamentals/coding-style/coding-conventions) and the [.NET naming guidelines](https://learn.microsoft.com/dotnet/standard/design-guidelines/naming-guidelines). Contributions must comply with them and with the repository rules below. + +**Enforced automatically.** The CI `verify` job fails a pull request that violates any of these, and the `main` ruleset requires that job: + +- The compiler: nullable reference types are on and every warning, including use of an obsolete API, is an error ([`Directory.Build.props`](Directory.Build.props)). +- The C# rules in [`.editorconfig`](.editorconfig), checked by `dotnet format style`: no unused `using` directives, `using` directives outside the namespace, file-scoped namespaces, explicit accessibility modifiers, PascalCase types and members, and an `I` prefix on interfaces. + +```bash +dotnet format style postgresql-sharp-mcp.slnx --verify-no-changes --no-restore --severity warn +dotnet format style postgresql-sharp-mcp.slnx --no-restore # applies the fixable ones +``` + +The same file sets UTF-8, LF line endings, four-space indentation (two for YAML, JSON, project files and Markdown), a final newline and no trailing whitespace for editors that read EditorConfig. The repository currently satisfies those settings, but they are not part of the automated check. + +**Followed in review.** Match the surrounding file for layout and brace style, which the check does not prescribe. Comments explain why, not what. Keep the `Core <- Tools <- App` dependency direction, keep MCP stdout for JSON-RPC and diagnostics on stderr, and keep maintained automation in C#. Every `.cs` file starts with the two-line header below; add it to new files. + +```csharp +// SPDX-FileCopyrightText: 2026 codegiveness +// SPDX-License-Identifier: MIT +``` + +**Exceptions** must be rare and justified. Use a narrowly scoped `#pragma warning disable` or `[SuppressMessage]`, or an `.editorconfig` severity entry, with a comment saying why; reviewers treat it like any other change. No exceptions exist today. Do not weaken the compiler settings or the check to make a build pass. + ## Issues, pull requests and reviews [Git and GitHub workflow](docs/git-workflow.md) explains step by step how a change moves through this repository: branch names, worktrees, commit messages, issue and pull-request lifecycle, squash merging, Dependabot, tags and releases, and branch cleanup. The issue forms and pull-request template ask for the information below. @@ -205,6 +230,29 @@ The database hostname must be reachable from the container; `localhost` refers t Keep versions, affected help/docs and release notes consistent. Update [THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md) and bundled licenses when shipped dependencies change. Contributions are under the project's [MIT license](LICENSE); dependencies retain their own terms. +### Code review standards + +Every change to `main` is reviewed before it merges, whoever wrote it. The repository enforces pull requests with passing required checks and resolved conversations. It does not enforce an independent approval: GitHub does not let an author approve their own pull request, so `main` requires zero approvals and the maintainer's own pull requests are self-reviewed ([why](docs/git-workflow.md#6-review)). Anyone may review and comment on any pull request, and an independent review is welcome and recorded in the pull request. + +**How review is conducted.** The reviewer reads the whole diff in **Files changed**, not only the description, and checks each claim under **Evidence** against the CI logs or by rerunning it. Pull requests written by AI agents carry the `agent-authored` label and are reviewed as a contribution from a capable stranger ([git workflow](docs/git-workflow.md#6-review)). + +**What must be checked.** + +- Every hunk traces to the stated problem; nothing unrequested is included. +- Behavior, compatibility and security impact are stated and match the diff, including defaults, permissions, the read-only boundary and output formats. +- Major new functionality or a bug fix has consumer-visible regression coverage in the verifier ([Verify behavior](#verify-behavior)), and the stated commands and scenarios actually ran. +- Docs, `--help`, `CHANGELOG.md` and [THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md) are updated where affected. +- No credentials, real connection strings, customer SQL or database results appear anywhere in the change or its description. +- Workflow changes are reviewed line by line: permissions, triggers, full-SHA action pins with version comments, and what runs with secrets or write tokens. +- Dependency updates have their release notes and advisory impact read, and locked restore still passes. +- The [coding standards](#coding-standards) hold. + +**What makes a change acceptable.** All required checks pass (build and integration, installation on Linux/Windows/macOS, dependency audit, CodeQL gate, secret and container scanning, fuzzing), conversations are resolved, the title follows the [commit format](docs/git-workflow.md#commits), and the reviewer has no unanswered finding about correctness, security or maintainability. The maintainer decides when those conditions hold and merges with Squash and merge. + +### Small tasks for new contributors + +Issues suited to a first contribution carry the [`good first issue`](https://github.com/codegiveness/postgresql-sharp-mcp/labels/good%20first%20issue) label, and larger tasks where help is wanted carry [`help wanted`](https://github.com/codegiveness/postgresql-sharp-mcp/labels/help%20wanted). A small task can be a documentation fix, a missing verifier scenario or a contained code change. If none is open, say what you would like to work on in [Discussions Q&A](https://github.com/codegiveness/postgresql-sharp-mcp/discussions/categories/q-a) before starting. + ## Automation and releases Compatible fixes use a patch increment, such as **0.3.2 → 0.3.3**; reserve a minor increment for new or incompatible 0.x contracts. Discovery and the unrestricted-default contract remain the capability boundary introduced by **0.3.1**, the first release that contains it (source version 0.3.0 was never tagged or released). **0.3.3** removes npm's installed-.NET prerequisite without changing database-access behavior; the .NET tool still requires .NET. **0.4.0** adds the database lock and its startup PostgreSQL grant check, which can refuse startup for existing `POSTGRES_DATABASES` configurations. A version in source is not evidence of NuGet/npm/GitHub Release publication; check the corresponding registry endpoint and release jobs. diff --git a/GOVERNANCE.md b/GOVERNANCE.md new file mode 100644 index 0000000..8401399 --- /dev/null +++ b/GOVERNANCE.md @@ -0,0 +1,82 @@ +# Governance + +This document says how decisions are made in postgresql-sharp-mcp, who holds which role, and what happens if the maintainer is unavailable. It describes the project as it is on 2026-10-11, not as it might become. The project has one maintainer; where that limits what it can promise, the text says so. + +## Governance model + +postgresql-sharp-mcp follows a **single-maintainer (benevolent dictator) model**. The maintainer, the GitHub user [`codegiveness`](https://github.com/codegiveness), owns the repository, makes the final decision on every change and releases the software. Anyone may propose a change, report a problem or review someone else's work; none of that creates authority over the project. The software is MIT-licensed ([LICENSE](LICENSE)), so anyone who disagrees with a decision can fork it. + +The repository's settings enforce the maintainer's own process: `main` accepts changes only through pull requests with passing required checks, force pushes and deletion are blocked, and release tags are immutable ([git workflow](docs/git-workflow.md#repository-settings)). Those rules apply to the maintainer as well. They are not independent review: GitHub does not let an author approve their own pull request, so `main` requires zero approvals. + +## How decisions are made + +1. **Proposal.** Open an issue or a discussion, or send a pull request directly for a small correction. [CONTRIBUTING.md](CONTRIBUTING.md#issues-pull-requests-and-reviews) lists what a useful proposal contains. Hard-to-reverse or large changes need the user need and the trade-offs written down first. +2. **Discussion.** Comments, reviews and evidence are welcome from everyone. The maintainer weighs them on correctness, security, compatibility with the documented contracts (including the [trust boundary](SECURITY.md#trust-boundary)), maintenance cost and scope. +3. **Decision.** The maintainer accepts, requests changes to, or declines the proposal and records the reason on the issue or pull request. Merging is a maintainer decision ([git workflow](docs/git-workflow.md#7-merge)). +4. **Record.** User-visible behavior goes into [CHANGELOG.md](CHANGELOG.md); security-relevant decisions go into [SECURITY.md](SECURITY.md) or [docs/security-posture.md](docs/security-posture.md). + +Disagreements are settled in the open, on the issue or pull request where they arose. If a discussion stalls, the maintainer decides and says why. A contributor who is still unhappy can keep a fork. Conduct complaints follow the [code of conduct](CODE_OF_CONDUCT.md#enforcement). That process currently ends with the maintainer, so a complaint about the maintainer's own conduct has no independent route inside the project; use [GitHub's abuse reporting](https://docs.github.com/en/communities/maintaining-your-safety-on-github/reporting-abuse-or-spam) for it. + +Changes to this document are made like any other change: by pull request, decided by the maintainer. + +## Roles and responsibilities + +| Role | Who holds it today | Responsibilities and authority | +|---|---|---| +| **Maintainer** | `codegiveness` (the only holder) | Final say on scope, design and merges. Triages issues and discussions, reviews pull requests, enforces the code of conduct, handles private vulnerability reports ([SECURITY.md](SECURITY.md#reporting-a-vulnerability)), keeps dependencies and the build current, dispatches and verifies releases, approves each npm publication with 2FA, and owns the GitHub repository, the `@codegiveness` npm packages and the NuGet package. Administers repository settings and decides who else receives any of these permissions. | +| **Contributor** | Anyone who opens an issue, discussion or pull request | Follows [CONTRIBUTING.md](CONTRIBUTING.md) and the [code of conduct](CODE_OF_CONDUCT.md); states the problem, the behavior change, the evidence and the security impact; never includes credentials, real connection strings, customer SQL or database results. Contributions are under the [MIT license](LICENSE). No authority to merge or release. | +| **Reviewer** | Any contributor who comments on or reviews a pull request | Reports actionable correctness, security or maintainability findings with reasons and, where possible, a reproduction, following the [code review standards](CONTRIBUTING.md#code-review-standards). Reviews inform the maintainer's decision; they do not replace it. | +| **Security reporter** | Anyone using private vulnerability reporting | Reports privately with a sanitized reproduction against disposable data. Is credited when the report is resolved unless they ask not to be ([SECURITY.md](SECURITY.md#how-reports-are-handled)). | +| **Automation** | Dependabot, GitHub Actions workflows | Proposes dependency updates and runs checks. A bot has no authority to approve, merge, release or change settings; Dependabot pull requests are reviewed like any other ([CONTRIBUTING.md](CONTRIBUTING.md#automation-and-releases)). | +| **AI coding agent** | Used by the maintainer, as a tool | Works only inside the authority granted for the task, in its own worktree and branch, per [AGENTS.md](AGENTS.md) and the [What agents may do](docs/git-workflow.md#what-agents-may-do) table. Agent-written pull requests carry the `agent-authored` label and say so. The maintainer remains accountable for everything merged. Agents never push, merge, tag, release, publish or change settings without explicit authorization. | + +`.github/CODEOWNERS` lists `@codegiveness` for every path. Code-owner review is not required, because that would make a single maintainer unable to merge ([repository settings](docs/git-workflow.md#not-recommended-now)). + +### Becoming a maintainer + +There is no quota and no automatic promotion. The maintainer may invite a contributor who has made sustained, substantive contributions and reviews, shown sound judgment on security-sensitive changes and followed the code of conduct. Before any escalated permission is granted (repository write or admin access, npm package maintainer, NuGet package owner, access to the `release` environment secrets), the invitee must have two-factor authentication on the relevant accounts, and the maintainer records the grant on the pull request or issue that proposes it, with the permission and scope named. The maintainer may remove a permission at any time. Adding a second maintainer is the single change that would improve every continuity limit below. + +## Continuity if the maintainer is unavailable + +**Status on 2026-10-11: the access-continuity requirement is not met, and the bus factor is 1.** No second person has the access needed to continue the project. This section records what that means so that users can judge the risk, and what exactly would change it. + +### What depends on the maintainer alone + +| Needed within a week of confirmed loss of the maintainer | Who can do it today | +|---|---| +| Create and close issues, label and triage | Any GitHub user can open issues and comment; only the maintainer (admin) can close others' issues, label or moderate | +| Accept proposed changes (merge to `main`) | Only the maintainer. The `main` ruleset requires a pull request, up-to-date required checks and resolved conversations, and has no bypass actors; changing it needs repository admin | +| Release a version | Only the maintainer. The [release workflow](.github/workflows/release.yml) is dispatched from `main` by someone with write access and uses the repository's `release` environment | +| Publish to npm | Only the maintainer. Every public npm version needs a human 2FA approval on the maintainer's npm account ([release procedure](CONTRIBUTING.md#approve-each-npm-publication-on-windows)); the packages' trusted-publisher settings and 2FA-only publishing access are also owner-controlled | +| Publish to NuGet | Only the maintainer. The trusted-publishing policy for `codegiveness.postgresql-sharp-mcp` is tied to the policy creator's NuGet profile (`NUGET_USERNAME` in the `release` environment) | +| Handle private vulnerability reports and code-of-conduct reports | Only the maintainer; both routes end with the maintainer ([SECURITY.md](SECURITY.md#reporting-a-vulnerability), [code of conduct](CODE_OF_CONDUCT.md#enforcement)) | + +Recovery material for the maintainer's own accounts (2FA recovery codes, hardware keys, password manager) is the maintainer's personal responsibility. This repository does not hold it and cannot show whether a trusted person could use it. + +### What reduces the damage + +- **The project can be forked and continued.** The source, history, CI definitions and release procedure are public and MIT-licensed. Nothing needed to build or verify the software is private. +- **No credentials live in the repository.** GitHub OIDC replaces long-lived npm and NuGet tokens; the remaining secret is `NUGET_USERNAME`, a username rather than a key ([CONTRIBUTING.md](CONTRIBUTING.md#independent-publisher-setup)). The secret scanner and Gitleaks check history. +- **Releases are verifiable without the maintainer.** Existing artifacts remain verifiable through their SHA-256 manifest and GitHub build attestations, and the release tool is built from the repository ([supply-chain evidence](docs/security-posture.md#supply-chain-evidence)). Already-published versions stay installable from GitHub, npm and NuGet. +- **The release procedure is written down**, so a successor with the right permissions can follow it ([git workflow](docs/git-workflow.md#tags-and-releases), [CONTRIBUTING.md](CONTRIBUTING.md#independent-publisher-setup)). + +These measures preserve users' ability to verify and fork. They do not let anyone merge, release or publish on the maintainer's behalf, so they do not satisfy the one-week continuity requirement. + +### What would change the status + +The requirement is met only when a second trusted person actually holds these permissions and could use them without the maintainer. Documentation alone does not meet it. Concretely: + +1. A second person with **admin** on `codegiveness/postgresql-sharp-mcp` and 2FA on their GitHub account, able to merge, edit the ruleset, dispatch `release.yml` and manage the `release` environment. +2. The same person added as a **maintainer of the six `@codegiveness/...` npm packages** (2FA required), able to approve staged versions, and able to edit the trusted-publisher configuration. +3. The same person as **co-owner of the NuGet package** with their own trusted-publishing policy for `release.yml` and the `release` environment, and their own `NUGET_USERNAME`. +4. A tested handover: the second person completes one real issue-to-release cycle (or a dry run of every step the maintainer can verify), and the maintainer records the date and result in this document. + +When all four hold, update this section and the OpenSSF Best Practices answers for `access_continuity` and `bus_factor` ([security posture](docs/security-posture.md#openssf-best-practices-enrollment)). Until then they stay Unmet. + +## Related documents + +- [CONTRIBUTING.md](CONTRIBUTING.md): contribution process, coding standards, code review standards, build and verification. +- [SECURITY.md](SECURITY.md): trust boundary, reporting and handling vulnerabilities. +- [docs/roadmap.md](docs/roadmap.md): direction for the next twelve months. +- [docs/security-posture.md](docs/security-posture.md): controls, Scorecard and OpenSSF Best Practices status. +- [docs/git-workflow.md](docs/git-workflow.md): branches, pull requests, merging and releases. diff --git a/README.md b/README.md index 6368ba9..fa302a6 100644 --- a/README.md +++ b/README.md @@ -621,6 +621,8 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for source builds, container usage, artif The server, npm installer, packaging, verification and release automation are C#/.NET. Workflow badges and [Scorecard](https://scorecard.dev/viewer/?uri=github.com/codegiveness/postgresql-sharp-mcp) report checks and practices, not certifications. Feature, platform and practice badges link to the documentation or configuration behind them; they describe implemented capabilities, not guarantees. [Best Practices](https://www.bestpractices.dev/en/projects/15155) is the saved owner self-assessment and may be in progress. See [Security posture](docs/security-posture.md) for [supply-chain evidence](docs/security-posture.md#supply-chain-evidence), SBOMs and limits, and [SECURITY.md](SECURITY.md) for reporting. +Project documents: [GOVERNANCE.md](GOVERNANCE.md) (decisions, roles and what happens if the maintainer is unavailable), [roadmap](docs/roadmap.md), [architecture](docs/architecture.md), [security assurance case](docs/assurance-case.md), [code of conduct](CODE_OF_CONDUCT.md) and [accessibility statement](ACCESSIBILITY.md). + Technical references: [Npgsql data sources](https://www.npgsql.org/doc/basic-usage.html), [pool parameters](https://www.npgsql.org/doc/connection-string-parameters.html), [sequential access](https://www.npgsql.org/doc/performance.html), [C# MCP SDK](https://github.com/modelcontextprotocol/csharp-sdk), [PostgreSQL EXPLAIN](https://www.postgresql.org/docs/current/sql-explain.html), [read-only transactions](https://www.postgresql.org/docs/current/sql-set-transaction.html), [pg_stat_statements](https://www.postgresql.org/docs/current/pgstatstatements.html) and [HypoPG](https://hypopg.readthedocs.io/en/latest/usage.html). Project source is MIT, copyright codegiveness. Dependency licenses remain separate: Npgsql uses the PostgreSQL license; the MCP SDK uses Apache-2.0. See [THIRD-PARTY-NOTICES.md](THIRD-PARTY-NOTICES.md) and bundled `LICENSES/` for versions, attribution and exact dependency terms. diff --git a/SECURITY.md b/SECURITY.md index 45501bb..7d250a2 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -147,3 +147,18 @@ Pools are keyed by normalized connection settings, including credentials/TLS and Do not post credentials, private data, or an exploit against an operational database in a public issue. Private vulnerability reporting is enabled for this repository: open [Security and quality](https://github.com/codegiveness/postgresql-sharp-mcp/security), then **Report a vulnerability**, following [GitHub's private-report instructions](https://docs.github.com/en/code-security/how-tos/report-and-fix-vulnerabilities/report-privately). No particular response time is guaranteed. If the private route becomes unavailable, open a minimal public issue asking the maintainer for a confidential contact route, without sensitive details. A useful report includes the affected version or commit, operating system and PostgreSQL version, configuration with secrets removed, the security boundary crossed, and a minimal reproduction using disposable data. Do not test systems without authorization. This document describes implementation protections and limitations; it is not an independent security audit or certification. + +## How reports are handled + +The maintainer is the only person who sees a private report. This is the process, in order; no step has a guaranteed duration. + +1. **Receipt and triage.** The maintainer reads the private report, tries to reproduce it with disposable data and decides whether it crosses a boundary this document promises ([trust boundary](#trust-boundary), [PostgreSQL permissions are the authority](#postgresql-permissions-are-the-authority), [database lock](#database-lock-hardening)). Behavior that is already documented as a limitation, such as the SQL guard not being an authorization parser, is answered with that documentation and may become a documentation fix. The maintainer asks the reporter for missing details in the private thread. +2. **Fix.** A confirmed vulnerability is fixed privately when public discussion would help exploitation before a release exists, otherwise through the normal pull-request path. The fix includes a regression scenario in the verifier ([CONTRIBUTING.md](CONTRIBUTING.md#verify-behavior)). A vulnerability in a dependency is fixed by updating or replacing it, as Dependabot alerts, the dependency audit and the container scan surface it. +3. **Release.** The fix ships in a new patch release from `main` through the [release procedure](docs/git-workflow.md#tags-and-releases); older releases are not patched, so affected users upgrade ([upgrade guidance](README.md#upgrading-an-existing-installation)). The CHANGELOG entry and GitHub Release notes name each fixed vulnerability. +4. **Disclosure and credit.** After a fixed release exists, the maintainer publishes a GitHub security advisory (requesting a CVE through GitHub when severity warrants one) and credits the reporter in the advisory and the CHANGELOG unless the reporter asks to stay anonymous. The reporter may propose a disclosure date; the maintainer decides it with the fix's availability in mind. + +Security fixes are the only changes that may be developed outside a public pull request before release. Everything else follows [docs/git-workflow.md](docs/git-workflow.md). + +## Security requirements and assurance case + +[docs/assurance-case.md](docs/assurance-case.md) lists the security requirements this software is meant to meet, the threat model and trust boundaries, and the argument, with evidence, that secure design principles were applied and common weaknesses are countered. It also records the residual risks and the gaps found while writing it. [docs/architecture.md](docs/architecture.md) describes the components those arguments refer to. diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..c95b08e --- /dev/null +++ b/docs/architecture.md @@ -0,0 +1,92 @@ +# Architecture + +postgresql-sharp-mcp is a [Model Context Protocol](https://modelcontextprotocol.io/) server written in C#/.NET 10. An MCP client (an AI assistant or coding agent) starts it as a local child process and talks to it over **stdio**. The server turns nine tool calls into bounded PostgreSQL queries and returns structured JSON. This document describes the high-level design; [SECURITY.md](../SECURITY.md) states the guarantees and limits and [docs/assurance-case.md](assurance-case.md) argues why they hold. Names below are source files; read the code for details. + +Checked against the source of release 0.5.0 (commit `64c55c7`) on 2026-10-11. + +## Context + +```text + MCP client (assistant/agent) + | JSON-RPC over stdin/stdout + v + postgresql-sharp-mcp (one process) -- diagnostics on stderr only + | Npgsql (PostgreSQL wire protocol, TLS as configured by the operator) + v + PostgreSQL server(s): the authorization boundary +``` + +- The server has no network listener, no HTTP client, no telemetry and no update check. Its only outbound connections are the PostgreSQL connections the operator configures. +- stdout carries MCP JSON-RPC and nothing else. Logs and warnings go to stderr, and the MCP SDK's and Npgsql's own log categories are disabled so request/response bodies cannot be logged. +- There is no MCP-level authentication or per-tool authorization. Whoever controls the client controls the server; PostgreSQL roles decide what the configured login may do. + +## Components + +Three projects keep one dependency direction, `Core <- Tools <- App`: + +| Project | Responsibility | +|---|---| +| `src/PostgreSqlMcp.Core` | Configuration, database and SQL behavior, error shaping. Knows nothing about MCP. | +| `src/PostgreSqlMcp.Tools` | The nine MCP tools and their descriptions. Calls Core only. | +| `src/PostgreSqlMcp` | The executable: command-line options, `--validate`, startup checks, dependency wiring, stdio transport. | + +### Core + +| File | Role | +|---|---| +| `ServerOptions.cs` | Parses command-line options, `POSTGRES_*` environment variables, targets JSON/file and the database lock into one immutable options object, enforcing every bound and rejecting conflicting credential sources. | +| `DatabaseRegistry.cs` | Holds connection profiles. Normalizes each connection string and overrides the settings the server must own (pooling, timeouts, no multiplexing, no error detail, no parameter logging, application name). Enforces the database lock and resolves `target`/`database`. Keeps a bounded least-recently-used cache of Npgsql data sources. | +| `SqlExecutor.cs` | `SqlExecutor` gates each call (access mode, concurrency limit, one deadline covering queue, connect and execution) and owns the transaction lifecycle. `SqlSession` is the only route to SQL: it validates the statement, batches transaction setup with the first statement, wraps paging and reads results through row, byte, cell and column limits. | +| `SqlGuard.cs` | A statement-boundary lexer. Accepts exactly one statement, understands comments, quoted text and dollar quoting, and rejects transaction/session control and some unsupported commands. It is not an authorization parser or a sandbox. | +| `SqlParameters.cs` | Binds up to 256 JSON scalars to `$1..$n` as text of unknown type. Values are never interpolated into SQL. | +| `ToolReply.cs` | The result envelope and error mapping. Errors keep the SQLSTATE and a fixed summary; PostgreSQL text is forwarded only where it provably comes from the caller's own statement. Caps the serialized result size. | +| `QueryPage.cs` | Result records: columns, rows, truncation, `next_offset`. | +| `DatabaseLockCheck.cs` | At startup and in `--validate`, proves in PostgreSQL that a locked login cannot reach other databases (superuser, server-file roles, `CREATEROLE`/`CREATEDB`, owned or connectable databases, `dblink`/`postgres_fdw`, server-file function grants). | +| `SecurityWarnings.cs` | Non-blocking stderr warnings: non-loopback hosts without certificate verification, and privileged role capabilities. | + +### Tools + +`ToolRegistration.cs` registers the public methods of four classes and sets the MCP annotations (read-only/destructive) and mode-dependent descriptions and server instructions from the configured access mode and lock. + +| Class | Tools | +|---|---| +| `DatabaseTools` | `list_databases`, `list_schemas`, `list_objects`, `get_object_details` (catalog queries; every user-controlled name is a bound parameter, and sections/types are fixed allowlists) | +| `SqlTools` | `execute_sql` | +| `PlanTools` | `explain_query` (plans, optional `EXPLAIN ANALYZE`, optional HypoPG hypothetical indexes under a savepoint with guaranteed cleanup) | +| `OpsTools` | `analyze_db_health`, `get_top_queries`, `analyze_indexes` | + +### App + +`Program.cs` prints `--help`/`--version`, parses options, optionally runs `--validate` (a read-only connectivity query plus the privilege/TLS warnings and the lock check), and otherwise runs the lock check, registers the services and starts the MCP SDK on stdio. Configuration errors exit with a fixed message on stderr. + +## Request flow + +1. The client sends `tools/call` on stdin; the MCP SDK dispatches it to the tool method. +2. The tool method runs inside `ToolReply.Run`, which converts every outcome into the JSON envelope. +3. The tool calls `SqlExecutor`. It resolves `target` and `database` through `DatabaseRegistry`, which rejects anything outside the database lock, and refuses writes in restricted mode. +4. The call waits for a concurrency slot within its deadline, leases a pooled connection for exactly that physical database and begins a transaction. +5. The first batch sets `SET TRANSACTION READ ONLY` (or `READ WRITE` for an explicit write in unrestricted mode) and the statement and lock timeouts, then runs the single validated statement. +6. The reader enforces row, byte, cell and column budgets. Read-only calls roll back; an explicit write commits only after successful execution and result reading. +7. `ToolReply` serializes the page, checks the byte cap and writes the response to stdout. + +Each call independently resolves its database. There is no shared current database, no fallback after a failed selection and no connection string accepted from tool arguments. + +## Configuration and credentials + +A base connection string from the environment creates the `primary` profile; optional targets JSON/file defines up to 32 independent profiles; an optional database lock (`POSTGRES_DATABASES`) restricts which physical databases may ever be opened. Combining a base connection string with targets is rejected rather than silently preferring one. Credentials come from the environment, an owner-protected file or the command line, never from tool arguments, and can be replaced without recompiling. See the [README configuration reference](../README.md#configuration-reference). + +## Build, packaging and verification + +- `tools/PostgreSqlMcp.Build` builds the npm packages (one main package plus five platform packages) and archives; `tools/PostgreSqlMcp.NpmInstall` is the installer those packages run; `tools/PostgreSqlMcp.Release` publishes and reconciles GitHub, npm and NuGet artifacts; `tools/Shared` holds the common process runner. All are C#. +- `tools/PostgreSqlMcp.Verify` is the test harness: a disposable PostgreSQL fixture (Docker), a stdio MCP client and 19 live scenario groups, plus offline groups for subprocess handling, the SARIF gate and release logic. `tools/PostgreSqlMcp.Fuzz` is the SharpFuzz/libFuzzer campaign over `SqlGuard`. +- CI (`.github/workflows/`) runs the verifier, package installation on Linux/Windows/macOS, CodeQL, dependency audit, secret scanning, container scanning, fuzzing and Scorecard. The release workflow builds, attests and publishes ([security posture](security-posture.md#supply-chain-evidence)). + +## Key properties and where they live + +| Property | Mechanism | +|---|---| +| PostgreSQL is the authority | The server sets read-only transactions and limits; roles, grants and RLS decide access | +| Bounded resources | Row/byte/cell/column limits, per-call deadline, concurrency limit, pool and cache bounds | +| No data in diagnostics | Fixed error summaries, no SDK/Npgsql logging, no parameter logging, stdout reserved | +| Fail closed | Invalid configuration, conflicting credential sources, lock violations and unverifiable lock checks stop startup or the call | +| Memory safety | Managed C#; no `unsafe` code or native interop in `src/` | diff --git a/docs/assurance-case.md b/docs/assurance-case.md new file mode 100644 index 0000000..9358ac3 --- /dev/null +++ b/docs/assurance-case.md @@ -0,0 +1,158 @@ +# Security assurance case + +This document argues why postgresql-sharp-mcp meets its security requirements, and says plainly where it does not. It contains the security requirements (what you can and cannot expect), the threat model, the trust boundaries, an argument that secure design principles were applied, an argument that common implementation weaknesses are countered, and the residual risks. Each claim points to code, documentation or a check you can rerun. + +**Status.** Drafted on 2026-10-11 against release 0.5.0 (commit `64c55c7`) by an AI coding agent working for the maintainer, from the source and the project's documents. It is a self-assessment, not an independent audit or certification. The [review record](#review-record) shows whether the maintainer has reviewed it. Report errors through the process in [SECURITY.md](../SECURITY.md#reporting-a-vulnerability). Component descriptions are in [docs/architecture.md](architecture.md). + +## 1. Security requirements + +The software is a local stdio MCP server that exposes PostgreSQL operations using one configured database login. These are the requirements it is meant to meet (R) and the expectations it deliberately does not meet (N). + +| ID | Requirement | +|---|---| +| R1 | **PostgreSQL is the authorization boundary.** The server never gives a caller more than the configured role's privileges, and never accepts credentials, hosts or TLS settings from tool arguments. | +| R2 | **Read-only by default.** SQL runs in a server-owned read-only transaction unless the operator leaves the default unrestricted mode and the caller explicitly sets `read_only=false`. Restricted mode refuses writes. Failures never commit. | +| R3 | **Database isolation.** Each call uses exactly one physical database selected for that call. With a database lock, the server opens no connection outside the lock and refuses to start when PostgreSQL grants do not enforce it. | +| R4 | **Credentials and data stay out of diagnostics.** Logs and errors contain no connection strings, passwords, SQL values or row data beyond what an authorized, successful result returns. stdout carries only MCP JSON-RPC. | +| R5 | **Bounded resource use.** Result size, statement duration, concurrency and connection counts are bounded, so one call cannot exhaust the host through the server's own behavior. | +| R6 | **Inputs are validated.** Every tool argument, option and configuration value is checked against an allowlist, length or range, and values reach PostgreSQL only as bound parameters or fixed SQL text. | +| R7 | **Transport security is delegated and visible.** TLS and authentication come from Npgsql and .NET under the operator's connection-string settings; the server warns on unverified remote connections. | +| R8 | **Released artifacts are verifiable.** Each release has SHA-256 checksums, an SBOM and signed build provenance, built from locked dependencies by pinned automation. | +| R9 | **Development is gated.** Changes reach `main` through pull requests with passing build, test, static analysis, dependency, secret and container checks. No secrets are stored in the repository. | + +| ID | Not expected | +|---|---| +| N1 | **A SQL sandbox.** The SQL guard is a statement-boundary lexer. Read-only transactions do not stop external effects of privileged routines or extensions, and `EXPLAIN ANALYZE` executes the query ([SECURITY.md](../SECURITY.md#postgresql-permissions-are-the-authority)). | +| N2 | **Protection against a malicious or compromised client.** There is no MCP authentication or per-tool authorization; whoever controls the client can use the login. | +| N3 | **Protection from over-privileged roles.** A superuser or a member of `pg_execute_server_program`, `pg_read_server_files` and similar roles can act beyond SQL reads. `--validate` warns; it does not prevent. | +| N4 | **Redaction of successful results.** Authorized results may contain sensitive data and are returned as-is within the limits. | +| N5 | **Verified TLS by default.** Npgsql's default `SSL Mode=Prefer` does not verify the server and can fall back to plaintext ([residual risks](#7-residual-risks-and-known-gaps)). | +| N6 | **Resilience against expensive queries or a total memory/CPU ceiling.** Limits bound the server's behavior, not what PostgreSQL does with an expensive statement. | +| N7 | **Defense against prompt injection.** The server cannot tell whether a tool call or returned data was influenced by hostile text. Read-only defaults and the database lock limit the consequences. | + +## 2. Assets, actors and threats + +**Assets:** data reachable by the configured role; the connection credentials; the integrity of the target databases; the availability of the host and database; the integrity of released artifacts and of the repository. + +**Actors:** the operator who configures the server; the MCP client and, through it, an AI model that may be steered by untrusted text; other local users and processes on the host; a network attacker between server and PostgreSQL; a malicious or compromised dependency, build tool or contributor; a vulnerability reporter. + +| ID | Threat | Countered by | Residual | +|---|---|---|---| +| T1 | A model, steered by hostile text, issues destructive or exfiltrating SQL | R2 default read-only transactions, restricted mode, role least privilege, row/byte limits | Authorized reads can still leak data to the client; unrestricted mode plus `read_only=false` allows writes the role permits | +| T2 | SQL written to escape the read-only transaction or change session state | `SqlGuard` rejects multiple statements and transaction/session control; PostgreSQL enforces the read-only transaction | `set_config()` and privileged routines are not blocked; the guard is not an authorization parser | +| T3 | Reaching other databases on the server than intended | R3 lock enforced in every path that opens a connection, plus startup proof in PostgreSQL | Startup snapshot: later grant changes are not re-verified until restart or `--validate` | +| T4 | Credential disclosure through logs, errors, process list or files | R4; configuration errors use fixed text; Npgsql error detail and parameter logging forced off; SDK logging disabled | Command-line connection strings appear in process arguments; environment and files are plaintext readable by privileged local users | +| T5 | Network attacker impersonates or sniffs the PostgreSQL connection | Operator sets `SSL Mode=VerifyFull`; server warns on non-loopback hosts without verification | Default `Prefer` is unverified and may be plaintext; the server does not force TLS | +| T6 | Resource exhaustion through large results, long statements, many concurrent calls or pool growth | R5 limits and deadlines | PostgreSQL still executes expensive statements until the statement timeout | +| T7 | Malformed configuration or arguments cause misbehavior | R6 allowlists, bounds, fixed errors, rejection of conflicting sources | None known beyond the documented limits | +| T8 | A compromised dependency, action, build tool or release path ships malicious code | Locked NuGet restore, Dependabot, dependency audit, full-SHA-pinned actions, checksum-pinned downloaded tools, container scan, attested releases, OIDC publishing without long-lived tokens | Trust in upstream packages and in the maintainer's account; single maintainer | +| T9 | A malicious pull request runs code with secrets or write access | No `pull_request_target`/`workflow_run` execution of untrusted code, read-only default tokens, `persist-credentials: false`, fork workflows need owner approval | Maintainer error or deliberate workflow weakening is not prevented ([security posture](security-posture.md#development-controls)) | +| T10 | Loss of the maintainer's access stops fixes | Fork-ability, public procedures | Not mitigated: bus factor 1 ([governance](../GOVERNANCE.md#continuity-if-the-maintainer-is-unavailable)) | + +## 3. Trust boundaries + +```text + B1 MCP client <---stdio---> server process client is the trusted principal + B2 server process <---Npgsql/TLS---> PostgreSQL PostgreSQL decides authorization + B3 server process <---> host OS (env, files, logs) protected by OS permissions + B4 maintainer/CI ---> release artifacts ---> users verified by checksums and attestations + B5 contributors ---> repository and CI privileges gated by PRs, checks and token scopes +``` + +- **B1.** Everything arriving on stdin is untrusted input from a trusted principal's session. It is validated, bounded and never turned into connection settings. +- **B2.** Data returned by PostgreSQL is untrusted: column counts, name lengths, types, cell sizes and plan depth are bounded before use; error text is gated. +- **B3.** Environment variables, command-line arguments and targets files are operator-controlled but parsed defensively with fixed diagnostics. The server does not check file permissions; the operator must protect the files. +- **B4.** Users should verify downloads against `SHA256SUMS` and attestations ([how](security-posture.md#supply-chain-evidence)). +- **B5.** Untrusted pull-request code runs only in read-only jobs; release, attestation and publishing jobs run from `main` and consume immutable artifacts. + +## 4. Argument that the requirements are met + +| Req. | Mechanism | Evidence | +|---|---|---| +| R1 | Connection strings come only from options parsed at startup (`ServerOptions.cs`). `DatabaseRegistry.cs` resolves `target`/`database` to an existing profile and physical name; tool arguments have no connection-string, host or credential parameter. | `Verify`: Discovery, Configuration and target-isolation scenarios (`tools/PostgreSqlMcp.Verify/Integration.cs`) | +| R2 | `SqlSession` is the only route to SQL and sends `SET TRANSACTION READ ONLY` in the first batch before the statement; restricted mode refuses `read_only=false`; read-only calls roll back, writes commit only after success. Setup and statement share one batch, so no caller can run SQL first. | `Verify`: Sql, Writes and rollback scenarios | +| R3 | The lock is checked in `Resolve`, again in the data-source cache before any pool exists, and for discovery. `DatabaseLockCheck.cs` probes PostgreSQL grants and refuses startup on any path around the lock. | `Verify`: `DatabaseLockChecks.cs` (connection-string and targets modes, refusals, no-fallback) | +| R4 | `ToolReply.cs` returns SQLSTATE plus fixed summaries; PostgreSQL text only for class-42 errors positioned inside the caller's statement or `42501`, and not from routines or internal queries. Npgsql `IncludeErrorDetail=false`, `LogParameters=false`; MCP and Npgsql log categories off; only `Program.cs` writes to stderr. | `Verify`: log-confidentiality and sanitized-error scenarios | +| R5 | Limits: SQL 131,072 characters; 256 parameters; rows 1,000 (max 5,000); result 64 KiB (max 1 MiB); cell 4,096 characters; 128 columns; offset 1,000,000; query timeout 30 s (max 600 s); 16 concurrent calls; pool size 8 and at most `floor(256 / pool size)` cached data sources; statement and lock timeouts set per transaction. | `Verify`: Boundaries, Pagination, Pools, PoolCapacity, Cancellation; `ResourceChecks.cs` | +| R6 | Allowlists for `order_by`, `section`, `type`, `format`, access mode; ranges for every numeric option; JSON scalars only for parameters; identifiers bound as parameters; only extension schema names are quoted into SQL text. `SqlGuard` is additionally fuzzed. | `Verify`: Parameters, FsCheck properties (`FuzzChecks.cs`); `tools/PostgreSqlMcp.Fuzz` with checked-in corpus | +| R7 | The code sets no TLS options and implements no cryptography; `SecurityWarnings.cs` warns for non-loopback hosts unless `VerifyCA`/`VerifyFull`. | `Verify`: SecurityWarnings scenarios | +| R8 | Release workflow builds locked dependencies into archives, npm and NuGet packages, a CycloneDX SBOM, `SHA256SUMS` and GitHub attestations; since 0.5.0 the attestation bundle is a release asset. | [Supply-chain evidence](security-posture.md#supply-chain-evidence); `release-regressions` verifier command | +| R9 | CI, CodeQL, dependency audit, Gitleaks, Trivy and fuzzing run on every PR and push to `main`; the `main` ruleset requires them. | Workflow runs on `64c55c7`, 2026-10-11: [CI](https://github.com/codegiveness/postgresql-sharp-mcp/actions/runs/38102103515), [CodeQL](https://github.com/codegiveness/postgresql-sharp-mcp/actions/runs/38102103505), [dependency audit](https://github.com/codegiveness/postgresql-sharp-mcp/actions/runs/38102103499), [secret scanning](https://github.com/codegiveness/postgresql-sharp-mcp/actions/runs/38102103514), [container security](https://github.com/codegiveness/postgresql-sharp-mcp/actions/runs/38102103513), [fuzzing](https://github.com/codegiveness/postgresql-sharp-mcp/actions/runs/38102103518) all succeeded | + +A passing run is a point-in-time result, not proof that no vulnerability exists. + +## 5. Secure design principles + +The eight principles of [Saltzer and Schroeder](https://web.mit.edu/Saltzer/www/publications/protection/), and input validation: + +| Principle | How it is applied | +|---|---| +| Economy of mechanism | About 2,300 lines of C# in `src/`, 16 files, three projects, one transport (stdio), no listener, no HTTP client, no plugin mechanism. | +| Fail-safe defaults | SQL calls default to `read_only=true`; invalid or conflicting configuration stops startup; failed database selection never falls back to another database; unknown options are rejected. The one default that is not deny-by-default is **unrestricted access mode when the mode is omitted**, which is documented, and writes still require an explicit `read_only=false`. TLS verification is also not the default ([N5](#1-security-requirements)). | +| Complete mediation | Every statement passes `SqlSession`, every connection passes `DatabaseRegistry` and its lock check, and every result passes the row/byte/cell budget and the response byte cap. PostgreSQL mediates object access on every statement. | +| Open design | The source, protocol, limits and weaknesses are public ([SECURITY.md](../SECURITY.md)); nothing relies on secrecy of the design. Secrets are only the operator's credentials, kept outside the repository. | +| Separation of privilege | Writing requires three independent conditions: unrestricted mode, an explicit `read_only=false` and PostgreSQL privileges. Releases need a main-branch dispatch plus a 2FA approval for npm. | +| Least privilege | The server asks for no more than a login's own rights and documents dedicated least-privileged roles; the database lock check refuses over-privileged logins. The container runs as a non-root user. Workflow tokens are read-only by default, with narrow job-level write scopes. | +| Least common mechanism | Calls share no current database or session state; connections are reset on close; pools are keyed by the full normalized connection settings including database and credentials. | +| Psychological acceptability | Defaults are safe to try (read-only transactions, bounded results); setup is three documented steps; errors say what to change without exposing data. | +| Input validation with allowlists | See R6. Denylists appear only in `SqlGuard`'s first-keyword rejection, which is a convenience on top of PostgreSQL's read-only enforcement, and it is documented as not an authorization mechanism. | +| Limited attack surface | Stdio only, nine tools, no file or process access from tool arguments, no server-side HTTP. | + +## 6. Common implementation weaknesses countered + +| Weakness | Status | How | +|---|---|---| +| [SQL injection (CWE-89)](https://cwe.mitre.org/data/definitions/89.html) | Countered for server-built SQL; user SQL is the product | Catalog and operations queries are fixed text with bound parameters; the extension schema is quoted with `QuoteIdentifier`; user-supplied `execute_sql`/`explain_query` text is the intended input and is confined by the read-only transaction, role grants and the guard, not by escaping. | +| [Improper input validation (CWE-20)](https://cwe.mitre.org/data/definitions/20.html) | Countered | R6; fixed diagnostics for parse failures. | +| [Sensitive information in error messages and logs (CWE-209](https://cwe.mitre.org/data/definitions/209.html), [CWE-532)](https://cwe.mitre.org/data/definitions/532.html) | Countered | R4; verified by confidentiality scenarios. | +| [Hard-coded credentials (CWE-798)](https://cwe.mitre.org/data/definitions/798.html) | Countered | None in the repository; Gitleaks with PostgreSQL URI/Npgsql rules scans all history; GitHub push protection is enabled. | +| [Cleartext storage or transmission of credentials (CWE-312](https://cwe.mitre.org/data/definitions/312.html), [CWE-319)](https://cwe.mitre.org/data/definitions/319.html) | Partly | The server never persists credentials. Environment values and files are plaintext by nature and documented as such; transmission security depends on `SSL Mode` (see N5). | +| [Improper certificate validation (CWE-295)](https://cwe.mitre.org/data/definitions/295.html) | Not countered by default | Operator-controlled; warning only. | +| [Uncontrolled resource consumption (CWE-400)](https://cwe.mitre.org/data/definitions/400.html) | Countered for the server; not for PostgreSQL | R5. | +| [OS command injection (CWE-78)](https://cwe.mitre.org/data/definitions/78.html) | Not applicable | `src/` starts no processes. Build and release tools start processes through one shared runner with explicit arguments and timeouts. | +| [Path traversal (CWE-22)](https://cwe.mitre.org/data/definitions/22.html) | Not applicable | The only file read is the operator-supplied targets file path; no tool argument names a file. | +| [Unsafe deserialization (CWE-502)](https://cwe.mitre.org/data/definitions/502.html) | Countered | Input is parsed only with `System.Text.Json` document parsing (nested plans are depth-limited); nothing is deserialized into polymorphic or type-name-driven objects. | +| [Buffer and integer errors (CWE-119](https://cwe.mitre.org/data/definitions/119.html), [CWE-190)](https://cwe.mitre.org/data/definitions/190.html) | Countered | Managed memory-safe C#, no `unsafe` or native interop in `src/`; numeric options are range-checked before use. | +| [Time-of-check/time-of-use (CWE-367)](https://cwe.mitre.org/data/definitions/367.html) | Partly | The lock is checked when a database is resolved and again before any pool is created. The PostgreSQL grant proof is a startup snapshot (see T3). | +| [Use of vulnerable components (CWE-1395)](https://cwe.mitre.org/data/definitions/1395.html) | Monitored | Locked restore, central versions, Dependabot alerts and updates, dependency audit with `NuGetAudit`, Trivy over the built container, pinned base-image digests. | +| [Download of code without integrity check (CWE-494)](https://cwe.mitre.org/data/definitions/494.html) | Countered | Tools downloaded in CI and the fuzz bridge are pinned by SHA-256; Actions are pinned by full commit SHA; the SDK is pinned in `global.json`; npm 12.2.0 is verified against its published SHA-512. | +| [Excessive privileges in automation (CWE-250)](https://cwe.mitre.org/data/definitions/250.html) | Countered | Workflow-level `permissions` are `contents: read` or empty; write scopes (`security-events`, `id-token`, `attestations`, `contents`) are granted per job for their single purpose. | + +Memory-safety and undefined-behavior hardening come from the language: C# is memory-safe, nullable reference types are enabled and `TreatWarningsAsErrors` makes compiler warnings, including use of obsolete APIs, build failures. The project has no HTTP interface, so web hardening headers do not apply. + +## 7. Residual risks and known gaps + +These are recorded here rather than argued away. Items marked **gap** are the ones the project would change if it could. + +1. **Gap: TLS defaults.** The server sets no `SSL Mode`. With Npgsql's default `Prefer`, the connection is encrypted only if the server offers TLS, and the certificate is not verified, so an on-path attacker can impersonate PostgreSQL and capture the login or downgrade to plaintext. The server warns on stderr for non-loopback hosts without `VerifyCA`/`VerifyFull` but does not refuse them. Operators should use `SSL Mode=VerifyFull`. This is why the OpenSSF criteria `crypto_certificate_verification`, `crypto_verification_private` and `crypto_used_network` are answered Unmet ([security posture](security-posture.md#openssf-best-practices-enrollment)). Verification by default for non-loopback hosts would be a behavior change needing an upgrade note. +2. **Legacy authentication.** Npgsql still answers a server's legacy MD5 request unless the operator sets `Require Auth` accordingly. +3. **Lock check is a snapshot.** Grants, roles and extensions changed after startup are enforced only by the in-process lock until the next start or `--validate`. +4. **The guard is a lexer.** Its first-keyword denylist does not cover `set_config()` or privileged routines. Isolation relies on PostgreSQL. +5. **Prompt injection** cannot be detected by the server (N7). +6. **Plaintext secrets at rest in process state.** Environment values, process arguments and targets files are exposed to sufficiently privileged local users; the server does not check targets-file permissions. +7. **Container image.** The Dockerfile installs a few runtime libraries without version pins; Trivy scans the built image on every run and fails on HIGH/CRITICAL findings. +8. **Test coverage is not measured.** The verifier exercises real behavior against PostgreSQL 13–18, but statement and branch coverage have not been computed, so untested paths cannot be enumerated. +9. **Single maintainer.** Review is self-review; there is no independent approval and no continuity if the maintainer is unavailable ([governance](../GOVERNANCE.md#continuity-if-the-maintainer-is-unavailable), [security posture](security-posture.md)). + +## 8. Re-verifying the evidence + +```bash +dotnet build postgresql-sharp-mcp.slnx -c Release -p:RestoreLockedMode=true # zero warnings +dotnet run --project tools/PostgreSqlMcp.Verify -c Release -- integration # needs Docker +dotnet run --project tools/PostgreSqlMcp.Verify -c Release -- sarif-regressions +dotnet run --project tools/PostgreSqlMcp.Verify -c Release -- release-regressions +dotnet run --project tools/PostgreSqlMcp.Fuzz -c Release -p:RestoreLockedMode=true -- campaign 60 +``` + +Hosted-run claims are tied to the run URLs above and to the revision they ran on; check the current `main` results rather than assuming they still hold. + +## 9. Keeping this document current + +Update it in the same pull request as any change to a requirement, default, limit, trust boundary, workflow privilege or release path, and review it at least once a year. Record each review below. + +### Review record + +| Date | Reviewer | Scope | Outcome | +|---|---|---|---| +| 2026-10-11 | AI coding agent (draft author) | Source of 0.5.0 at `64c55c7`, workflows, project documents | Draft written; not a human review | +| _pending_ | Maintainer | Requirements, boundaries, residual risks | _Not yet recorded. The OpenSSF `security_review` criterion needs a human review recorded here_ | diff --git a/docs/roadmap.md b/docs/roadmap.md new file mode 100644 index 0000000..3f7a348 --- /dev/null +++ b/docs/roadmap.md @@ -0,0 +1,40 @@ +# Roadmap + +This is the direction for the twelve months from 2026-10-11 to 2027-10-11, so that users and contributors can see what the project intends to do and not do. It is not a promise: the project has one maintainer ([governance](../GOVERNANCE.md)), the software is a 0.x release series, and dates depend on the maintainer's time. Items that need someone other than the maintainer are marked as such. The list is derived from gaps the project has documented itself in [docs/security-posture.md](security-posture.md), [docs/assurance-case.md](assurance-case.md) and the [OpenSSF Best Practices answers](security-posture.md#openssf-best-practices-enrollment). + +## What the project intends to do + +### Keep the supported surface current + +- Keep working with the PostgreSQL majors the integration fixture covers (13–18, see [supported versions](../README.md#supported-postgresql-versions)) and add newly released majors to the CI matrix when the fixture can pull them by digest. +- Stay on a supported .NET 10 SDK and runtime. Dependabot proposes SDK, NuGet, Docker and Actions updates weekly; each is reviewed against release notes and the verifier before merging. +- Track the C# MCP SDK and Npgsql releases, and the MCP protocol revisions that the SDK implements, without changing existing tool names, parameters or environment variables in compatible releases. +- Release compatible fixes as patch versions and reserve minor versions for new or incompatible 0.x contracts ([versioning](../CONTRIBUTING.md#automation-and-releases)). Breaking changes are listed under upgrade notes in the [CHANGELOG](../CHANGELOG.md). + +### Close the security gaps the project already knows about + +- **TLS defaults.** Npgsql's default `SSL Mode=Prefer` encrypts without verifying the server and can fall back to plaintext. Today the server only warns ([assurance case, residual risks](assurance-case.md#7-residual-risks-and-known-gaps)). The intent is to decide, with an upgrade note, whether connections to non-loopback hosts should require certificate verification unless the operator opts out explicitly. +- **Measured test coverage.** Statement and branch coverage of the integration run is not measured today. The intent is to collect it with a .NET FLOSS tool in CI, publish the result and then set a threshold. +- **Reproducible packages.** Archives and npm tarballs are byte-identical across rebuilds; the `.nupkg` and the SBOM are not. The intent is to make them deterministic. +- **Release provenance visibility.** Releases from 0.5.0 carry `provenance.sigstore.json`, which is how Scorecard's Signed-Releases check sees the signatures. Each new release extends that coverage. +- **Assurance case upkeep.** Review and update the assurance case whenever the trust boundary, a default or a release path changes, and at least once a year. + +### Sustain the project + +- Keep issues, discussions and pull requests triaged, and security reports handled as described in [SECURITY.md](../SECURITY.md#how-reports-are-handled). +- Keep the documentation consistent with the released version; a documentation defect found in review is treated as a bug. +- Welcome a second maintainer and contributors from other organizations. This depends on others joining and cannot be scheduled; [governance](../GOVERNANCE.md#continuity-if-the-maintainer-is-unavailable) lists what would change. + +## What the project does not plan to do + +These follow from the documented [trust boundary](../SECURITY.md#trust-boundary) and design: + +- **MCP-level authentication, per-tool authorization or tenant identity.** The client that launches the server is trusted; PostgreSQL roles are the authorization boundary. +- **Network transports.** The server speaks stdio only and opens no listener. Do not expose it through an unauthenticated bridge. +- **A SQL authorization parser or sandbox.** The SQL guard is a statement-boundary lexer. Isolation comes from read-only transactions, role grants and the optional database lock. +- **Storing or managing credentials.** The server reads them from the environment or an owner-protected file and does not persist them. +- **Other database engines and graphical interfaces.** The scope is PostgreSQL through MCP. + +## How to influence the roadmap + +Open an issue or a [discussion](https://github.com/codegiveness/postgresql-sharp-mcp/discussions) describing the problem and the trade-offs. A pull request with evidence is the most direct route. The maintainer decides what is accepted ([governance](../GOVERNANCE.md#how-decisions-are-made)). From 97f8c1a898198e8e1a38abc18868b1118c1c217e Mon Sep 17 00:00:00 2001 From: AG Date: Sun, 11 Oct 2026 09:21:11 +0700 Subject: [PATCH 03/12] docs: propose Silver and Gold answers and record observed OpenSSF figures Record live Scorecard and Best Practices figures with dates, the unmet criteria and exactly what would change each, and an owner action checklist. --- .bestpractices.json | 116 ++++++++++++++++++++++++++++++++++++++- docs/security-posture.md | 97 +++++++++++++++++++++++++++++++- 2 files changed, 209 insertions(+), 4 deletions(-) diff --git a/.bestpractices.json b/.bestpractices.json index 0bb76a9..8df23d9 100644 --- a/.bestpractices.json +++ b/.bestpractices.json @@ -133,5 +133,119 @@ "report_process_justification": "Public-source evidence: CONTRIBUTING tells users to submit bug reports as GitHub issues with the package version, sanitized configuration, minimal reproduction, expected and actual behavior and redacted diagnostics, and routes vulnerabilities to SECURITY: https://github.com/codegiveness/postgresql-sharp-mcp/blob/fbbe5aa0c2abea5d48d58fa282d84204921c8e19/CONTRIBUTING.md#issues-pull-requests-and-reviews and https://github.com/codegiveness/postgresql-sharp-mcp/issues", "delivery_mitm_status": "Met", "delivery_mitm_justification": "Public-source evidence: the software is delivered only over HTTPS, through GitHub Releases, npm, NuGet and git over HTTPS. Release assets also carry SHA-256 checksums and GitHub build attestations that users can verify with gh attestation verify, and npm publication requests provenance: https://github.com/codegiveness/postgresql-sharp-mcp/blob/fbbe5aa0c2abea5d48d58fa282d84204921c8e19/docs/security-posture.md#supply-chain-evidence and https://github.com/codegiveness/postgresql-sharp-mcp/releases", - "general_comments": "Evidence-backed project metadata for https://www.bestpractices.dev/en/projects/15155 . Proposed answers require review and saving on the badge website. Not inferred: developer knowledge beyond the owner's attestation; response timeliness for reports not yet received (0 issues, security advisories and Dependabot alerts and 0 open CodeQL alerts as checked through the GitHub API on 2026-10-10); negotiated TLS key lengths, algorithms or forward secrecy beyond Npgsql, .NET and OS defaults and the operator's SSL Mode and PostgreSQL server settings; absence of vulnerabilities or credentials beyond the cited point-in-time scans; branch coverage; and pre-release static analysis for v0.1.0, which predates the CodeQL workflow." + "dco_status": "Unmet", + "dco_justification": "This SHOULD criterion is not adopted. Contributions are accepted under the project's MIT license, stated in https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/CONTRIBUTING.md#issues-pull-requests-and-reviews , but no Developer Certificate of Origin sign-off or contributor license agreement is required or enforced. A documented DCO policy with sign-off enforcement would change this.", + "governance_status": "Met", + "governance_justification": "GOVERNANCE.md documents the single-maintainer governance model, how proposals are made, decided and recorded, and how disputes are handled: https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/GOVERNANCE.md#governance-model and https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/GOVERNANCE.md#how-decisions-are-made . This file is added by the pull request that proposes these answers; its URL resolves once that change is merged to main.", + "code_of_conduct_status": "Met", + "code_of_conduct_justification": "The repository adopts the Contributor Covenant 2.1 in the standard location, with an enforcement contact and enforcement guidelines: https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/CODE_OF_CONDUCT.md . CONTRIBUTING links it: https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/CONTRIBUTING.md#issues-pull-requests-and-reviews", + "roles_responsibilities_status": "Met", + "roles_responsibilities_justification": "GOVERNANCE.md defines the Maintainer, Contributor, Reviewer, Security reporter, Automation and AI-agent roles with their responsibilities and says who holds each (the maintainer role is held by the GitHub user codegiveness): https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/GOVERNANCE.md#roles-and-responsibilities . This file is added by the pull request that proposes these answers; its URL resolves once that change is merged to main.", + "access_continuity_status": "Unmet", + "access_continuity_justification": "Not met on 2026-10-11. No second person has the access needed to merge, release or publish if the maintainer is unavailable; GitHub admin, npm package maintainer and NuGet package ownership rest with one account. What exists (public MIT source, written release procedure, no stored credentials, verifiable artifacts) and exactly what would change the answer are recorded in https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/GOVERNANCE.md#continuity-if-the-maintainer-is-unavailable . This file is added by the pull request that proposes these answers; its URL resolves once that change is merged to main.", + "bus_factor_status": "Unmet", + "bus_factor_justification": "The bus factor is 1: the GitHub contributors API reported one human contributor (58 commits) and Dependabot (3) on 2026-10-11. See https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/GOVERNANCE.md#continuity-if-the-maintainer-is-unavailable for the steps that would raise it. This file is added by the pull request that proposes these answers; its URL resolves once that change is merged to main.", + "documentation_roadmap_status": "Met", + "documentation_roadmap_justification": "docs/roadmap.md describes what the project intends to do and not do for the twelve months from 2026-10-11 and says it is direction, not a promise: https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/docs/roadmap.md . This file is added by the pull request that proposes these answers; its URL resolves once that change is merged to main.", + "documentation_architecture_status": "Met", + "documentation_architecture_justification": "docs/architecture.md describes the components, dependency direction, request flow, configuration model and verification structure: https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/docs/architecture.md . This file is added by the pull request that proposes these answers; its URL resolves once that change is merged to main.", + "documentation_security_status": "Met", + "documentation_security_justification": "SECURITY.md states the trust boundary, what PostgreSQL permissions must enforce, credential and diagnostic handling, TLS responsibility and limits: https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/SECURITY.md#trust-boundary . docs/assurance-case.md lists the security requirements and the expectations the software deliberately does not meet: https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/docs/assurance-case.md#1-security-requirements . This file is added by the pull request that proposes these answers; its URL resolves once that change is merged to main.", + "documentation_quick_start_status": "Met", + "documentation_quick_start_justification": "README has a three-step quick start (install or run, prepare the connection environment, validate and start): https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/README.md#quick-start", + "documentation_current_status": "Met", + "documentation_current_justification": "Documentation is updated with the change that alters it. This is required by AGENTS.md and the pull-request checklist (https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/AGENTS.md , https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/.github/pull_request_template.md ), a documentation-versus-help mismatch was tracked as issue 41 and fixed in 0.5.0 (https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/CHANGELOG.md ), and documents that carry release-specific claims state the version and date they were checked against. This is an effort, not a guarantee of perfection.", + "documentation_achievements_status": "Met", + "documentation_achievements_justification": "The README front page hyperlinks the Best Practices entry and Scorecard and explains what they do and do not certify: https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/README.md#development-and-licensing . The Project badges table shows the badges: https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/README.md#project-badges", + "accessibility_best_practices_status": "Met", + "accessibility_best_practices_justification": "The software has no graphical interface; the project documents its command-line, MCP-client and documentation accessibility, known limitations and a barrier-reporting route: https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/ACCESSIBILITY.md . CLI output is plain text without colors or control codes.", + "internationalization_status": "Unmet", + "internationalization_justification": "The software produces English-only text (--help, startup warnings, tool summaries and error messages) with no resource files or localization mechanism. Tool results are structured JSON with SQLSTATE codes, and parsing and sorting use invariant or ordinal rules, but the user-facing strings are not externalized for translation. SHOULD criterion; not planned in https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/docs/roadmap.md .", + "sites_password_security_status": "N/A", + "sites_password_security_justification": "The project sites are GitHub, npmjs.com and nuget.org. The project operates no site that stores passwords of external users; the criterion notes that use of GitHub meets it.", + "maintenance_or_update_status": "Met", + "maintenance_or_update_justification": "The project is 0.x and ships fixes as new releases from main; older releases are not patched, so an upgrade path is provided and documented: https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/README.md#upgrading-an-existing-installation , upgrade notes in the https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/CHANGELOG.md entries where behavior changes (for example the breaking database-lock note in 0.4.0 and the upgrade notes in 0.5.0), and the upgrade boundary in https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/docs/security-posture.md#runtime-boundaries . https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/SECURITY.md#how-reports-are-handled states that affected users upgrade. SECURITY.md is changed by the pull request that proposes these answers; its new section's URL resolves once that change is merged to main.", + "vulnerability_report_credit_status": "N/A", + "vulnerability_report_credit_justification": "No vulnerability reports have been resolved in the last 12 months: the GitHub API showed 0 repository security advisories and 0 Dependabot alerts on 2026-10-11. The credit practice (advisory and CHANGELOG credit unless anonymity is requested) is documented in https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/SECURITY.md#how-reports-are-handled . This file is added by the pull request that proposes these answers; its URL resolves once that change is merged to main.", + "vulnerability_response_process_status": "Met", + "vulnerability_response_process_justification": "SECURITY.md documents the response process: private receipt, triage, fix with regression coverage, release, advisory and credit: https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/SECURITY.md#how-reports-are-handled . It guarantees no response time, which is stated. This file is added by the pull request that proposes these answers; its URL resolves once that change is merged to main.", + "coding_standards_status": "Met", + "coding_standards_justification": "CONTRIBUTING identifies Microsoft's C# coding conventions and the .NET naming guidelines as the style guides, requires contributions to comply and lists repository-specific rules and the exception policy: https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/CONTRIBUTING.md#coding-standards . This file is added by the pull request that proposes these answers; its URL resolves once that change is merged to main.", + "coding_standards_enforced_status": "Met", + "coding_standards_enforced_justification": "The repository's .editorconfig defines C# style rules (unused usings, using placement, file-scoped namespaces, accessibility modifiers, PascalCase naming, I-prefixed interfaces) and the CI verify job runs dotnet format style --verify-no-changes --severity warn, which fails on any violation; the compiler also treats all warnings, including obsolete API use, as errors: https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/.editorconfig , https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/.github/workflows/ci.yml , https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/Directory.Build.props . Brace and line-layout conventions are deliberately not prescribed (documented in CONTRIBUTING); no style exceptions exist. The step and files are added by the pull request that proposes these answers.", + "build_standard_variables_status": "N/A", + "build_standard_variables_justification": "The project does not build native binaries with a C or C++ compiler or linker. It is managed C# built by the .NET SDK and MSBuild, so CC, CFLAGS, CXXFLAGS and LDFLAGS do not apply.", + "build_preserve_debug_status": "Unmet", + "build_preserve_debug_justification": "SHOULD criterion. Source builds with dotnet build keep the SDK's debug-information defaults and the project sets no override (https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/Directory.Build.props ). The release packaging tool, however, forces DebugType=None and DebugSymbols=false for the shipped archives and packages and rejects archives containing .pdb files (https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/tools/PostgreSqlMcp.Build/Program.cs ), so debug information cannot be requested for release artifacts.", + "build_non_recursive_status": "Met", + "build_non_recursive_justification": "The build is an MSBuild solution whose projects declare explicit ProjectReference dependencies (Core <- Tools <- App); no recursive make or per-directory build scripts exist: https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/postgresql-sharp-mcp.slnx and https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/CONTRIBUTING.md#build-from-source", + "build_repeatable_status": "Unmet", + "build_repeatable_justification": "Partly met. Two clean Release builds on 2026-10-11 produced identical hashes for the application assemblies, and release archives and npm tarballs are byte-identical across rebuilds (CHANGELOG 0.5.0, covered by the release-regressions verifier command). The .nupkg (zip entry timestamps) and the CycloneDX SBOM (random serial number, timestamp) are not bit-for-bit repeatable, so the criterion is not met for every release output: https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/CONTRIBUTING.md#automation-and-releases . Making them deterministic would change this.", + "installation_common_status": "Met", + "installation_common_justification": "Installation and uninstallation use commonly used package managers: npm (npx, npm install -g/uninstall) and the .NET tool (dotnet tool install/uninstall), plus verified archives and a Dockerfile: https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/README.md#1-install-or-run and https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/README.md#other-installation-methods", + "installation_standard_variables_status": "N/A", + "installation_standard_variables_justification": "The project ships no DESTDIR-style installer. Installation locations are controlled by the standard conventions of the package managers used (npm prefix, dotnet tool --tool-path) or by where the user extracts an archive.", + "installation_development_quick_status": "Met", + "installation_development_quick_justification": "CONTRIBUTING gives the clone, restore, build and run commands for developers and the single command that builds and runs the full verifier against a disposable PostgreSQL fixture (Docker required): https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/CONTRIBUTING.md#build-from-source and https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/CONTRIBUTING.md#verify-behavior", + "external_dependencies_status": "Met", + "external_dependencies_justification": "Dependencies are listed in machine-readable form: central NuGet versions in https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/Directory.Packages.props , committed lock files per project and runtime identifier (for example https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/src/PostgreSqlMcp.Core/packages.lock.json ), and https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/npm/package.json for the npm packages.", + "dependency_monitoring_status": "Met", + "dependency_monitoring_justification": "Dependabot tracks NuGet, GitHub Actions, Docker and .NET SDK updates weekly (https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/.github/dependabot.yml ); Dependabot alerts and security updates are enabled (0 open alerts on 2026-10-11); the Dependency audit workflow runs NuGet audit over direct and transitive packages on pushes, pull requests and weekly (https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/.github/workflows/security.yml , success: https://github.com/codegiveness/postgresql-sharp-mcp/actions/runs/38102103499 ); Trivy scans the built container's OS and application packages (https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/.github/workflows/container-security.yml ).", + "updateable_reused_components_status": "Met", + "updateable_reused_components_justification": "Reused components are NuGet packages with central version management, locked restore and Dependabot updates, so a vulnerable component is identified and updated in one place: https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/Directory.Packages.props . The container's base images are pinned by digest and tracked by Dependabot (https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/Dockerfile ). Bundled licenses and versions are listed in https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/THIRD-PARTY-NOTICES.md", + "interfaces_current_status": "Met", + "interfaces_current_justification": "TreatWarningsAsErrors makes use of an obsolete or deprecated .NET API a build failure (compiler warning CS0618), and the project targets the current .NET 10 SDK pinned in global.json: https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/Directory.Build.props and https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/global.json", + "automated_integration_testing_status": "Met", + "automated_integration_testing_justification": "The CI workflow runs the real PostgreSQL integration suite and package installation checks on every push to main and every pull request and reports success or failure per job: https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/.github/workflows/ci.yml . Example: https://github.com/codegiveness/postgresql-sharp-mcp/actions/runs/38102103515 succeeded on 2026-10-11.", + "regression_tests_added50_status": "Met", + "regression_tests_added50_justification": "Checked with git history on 2026-10-11: 9 of the 11 fix: commits on main (all since the repository was created on 2026-10-02) changed the automated test scenarios, either the C# verifier in tools/PostgreSqlMcp.Verify (pull requests 11, 25, 45, 46, 49, 51 and 60) or the earlier smoke script it replaced (two earlier fixes). The other two changed release workflows, release tooling and documentation only. The contribution policy requires regression coverage for fixes: https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/CONTRIBUTING.md#verify-behavior . The window is nine days, so this is a small sample and changed scenarios are not individually proven to be regression tests for each bug.", + "test_statement_coverage80_status": "Unmet", + "test_statement_coverage80_justification": "Statement coverage is not measured. The verifier exercises real MCP calls against PostgreSQL 13 to 18, but no coverage tool is run in CI, so 80% cannot be claimed. Measuring coverage of the integration run with a .NET FLOSS tool and reaching the threshold would change this; see https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/docs/roadmap.md . This file is added by the pull request that proposes these answers; its URL resolves once that change is merged to main.", + "test_policy_mandated_status": "Met", + "test_policy_mandated_justification": "CONTRIBUTING and AGENTS.md require consumer-visible automated coverage for major new functionality and for plausible consumer-visible regressions, extending the maintained verifier: https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/CONTRIBUTING.md#verify-behavior and https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/AGENTS.md#implementation-and-evidence", + "implement_secure_design_status": "Met", + "implement_secure_design_justification": "docs/assurance-case.md applies each Saltzer and Schroeder principle, plus allowlist input validation and limited attack surface, to the design with code and test evidence: https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/docs/assurance-case.md#5-secure-design-principles . It records the exceptions (default unrestricted access mode and unverified default TLS). This file is added by the pull request that proposes these answers; its URL resolves once that change is merged to main.", + "crypto_algorithm_agility_status": "Met", + "crypto_algorithm_agility_justification": "The project implements no cryptography; algorithm and protocol selection is delegated to Npgsql, .NET and the operating system's TLS and to the PostgreSQL server (SCRAM-SHA-256, other methods, TLS versions and cipher suites), so operators can switch algorithms through server, OS or connection-string settings without code changes: https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/docs/assurance-case.md#3-trust-boundaries and https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/SECURITY.md#connection-and-resource-settings", + "crypto_credential_agility_status": "Met", + "crypto_credential_agility_justification": "Credentials are never hard-coded: they come from the environment, an owner-protected targets file or CLI options, separate from other configuration, and can be replaced by restarting the server without recompiling: https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/README.md#2-prepare-the-connection-environment and https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/SECURITY.md#credentials-and-diagnostics", + "crypto_used_network_status": "Unmet", + "crypto_used_network_justification": "The server's only network communication is the PostgreSQL connection, and TLS is supported but not required: the server sets no SSL Mode, and Npgsql's default Prefer encrypts only if the server offers TLS and can fall back to unencrypted (https://www.npgsql.org/doc/security.html , observed 2026-10-11). The server prints a warning for non-loopback hosts without VerifyCA or VerifyFull but does not refuse them: https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/docs/assurance-case.md#7-residual-risks-and-known-gaps . Insecure transport is therefore not disabled by default. Defaulting non-loopback connections to required, verified TLS (a behavior change needing an upgrade note) would change this.", + "crypto_tls12_status": "Met", + "crypto_tls12_justification": "TLS is performed by Npgsql through .NET SslStream; the server does not restrict protocol versions and leaves negotiation to the operating system, which on supported platforms offers TLS 1.2 and later: https://learn.microsoft.com/dotnet/core/extensions/sslstream-best-practices and https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/SECURITY.md#connection-and-resource-settings . Whether TLS is used at all is the operator's SSL Mode choice (see crypto_used_network).", + "crypto_certificate_verification_status": "Unmet", + "crypto_certificate_verification_justification": "Not by default. With Npgsql's default SSL Mode=Prefer, and with Require, TLS is used without certificate verification (https://www.npgsql.org/doc/security.html , observed 2026-10-11); verification happens only when the operator sets VerifyCA or VerifyFull, which the README examples and warnings recommend: https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/SECURITY.md#connection-and-resource-settings and https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/docs/assurance-case.md#7-residual-risks-and-known-gaps . Defaulting remote connections to VerifyFull would change this.", + "crypto_verification_private_status": "Unmet", + "crypto_verification_private_justification": "Because certificate verification is not the default (see crypto_certificate_verification), the login can be sent to an unverified server under the default SSL Mode, as SECURITY.md already warns. With SSL Mode=VerifyCA or VerifyFull Npgsql verifies the certificate during the TLS handshake, before authentication. See https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/SECURITY.md#connection-and-resource-settings .", + "signed_releases_status": "Met", + "signed_releases_justification": "Releases from v0.2.0 carry GitHub build attestations (Sigstore keyless signatures bound to the release.yml workflow identity and the exact source commit) for every archive, package, the SBOM and SHA256SUMS; since v0.5.0 the signed bundle is also published as the provenance.sigstore.json release asset (gh release view v0.5.0 listed it on 2026-10-11). No long-lived private signing key exists on the distribution sites. The documented verification process, which checks repository, signer workflow and subject digest with gh attestation verify, is in https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/docs/security-posture.md#supply-chain-evidence (the release verification commands near the end of that section). v0.1.0 predates attestations.", + "version_tags_signed_status": "Unmet", + "version_tags_signed_justification": "Release tags are annotated but not cryptographically signed: GitHub's tag API reported verified=false (reason unsigned) for v0.1.0, v0.2.0, v0.3.1, v0.3.2, v0.4.0 and v0.5.0 on 2026-10-11, and v0.3.3 is a lightweight tag. Signed tags (git tag -s with a signing key registered on GitHub) would change this.", + "input_validation_status": "Met", + "input_validation_justification": "Tool arguments, options and configuration are validated against allowlists, ranges and length limits and rejected otherwise; values reach PostgreSQL only as bound parameters or fixed SQL: https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/docs/assurance-case.md#4-argument-that-the-requirements-are-met (R6). The SQL guard is additionally fuzzed (https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/tools/PostgreSqlMcp.Fuzz/Program.cs ) and covered by FsCheck properties (https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/tools/PostgreSqlMcp.Verify/FuzzChecks.cs ). This file is added by the pull request that proposes these answers; its URL resolves once that change is merged to main.", + "hardening_status": "Met", + "hardening_justification": "The software is managed, memory-safe C# with no unsafe code or native interop in src; nullable reference types and warnings-as-errors remove classes of undefined behavior (https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/Directory.Build.props ); the server bounds results, statements, concurrency and pools and forces safe Npgsql settings so defects are less likely to become vulnerabilities (https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/docs/assurance-case.md#4-argument-that-the-requirements-are-met ). The project has no HTTP interface, so CSP and similar headers do not apply. Least privilege (non-root container, narrow tokens) is counted separately and not claimed as hardening. This file is added by the pull request that proposes these answers; its URL resolves once that change is merged to main.", + "assurance_case_status": "Met", + "assurance_case_justification": "docs/assurance-case.md contains the threat model, the trust boundaries, an argument that secure design principles were applied, an argument that common weaknesses are countered, and the residual risks and gaps: https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/docs/assurance-case.md . It is a self-assessment drafted by an AI agent for the maintainer and says so; the maintainer's review is recorded in the document. This file is added by the pull request that proposes these answers; its URL resolves once that change is merged to main.", + "contributors_unassociated_status": "Unmet", + "contributors_unassociated_justification": "The project has one human contributor; no second unassociated significant contributor exists (GitHub contributors API, 2026-10-11).", + "copyright_per_file_status": "Met", + "copyright_per_file_justification": "Every tracked .cs source file begins with SPDX-FileCopyrightText: 2026 codegiveness, matching the LICENSE copyright holder; CONTRIBUTING requires the header for new files: https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/CONTRIBUTING.md#coding-standards . Added to all 39 .cs files by the pull request that proposes these answers.", + "license_per_file_status": "Met", + "license_per_file_justification": "Every tracked .cs source file begins with SPDX-License-Identifier: MIT (for example https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/src/PostgreSqlMcp.Core/SqlGuard.cs ); the license text is https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/LICENSE . Added to all 39 .cs files by the pull request that proposes these answers. Non-source data files such as fuzz corpus seeds are not annotated, because changing their bytes would change the corpus.", + "small_tasks_status": "Unmet", + "small_tasks_justification": "The good first issue and help wanted labels exist and CONTRIBUTING explains them (https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/CONTRIBUTING.md#small-tasks-for-new-contributors ), but no open issue carries either label on 2026-10-11. Labeling real small tasks would change this.", + "code_review_standards_status": "Met", + "code_review_standards_justification": "CONTRIBUTING documents how review is conducted, what must be checked and what makes a change acceptable: https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/CONTRIBUTING.md#code-review-standards , with the self-review procedure in https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/docs/git-workflow.md#6-review . This file is added by the pull request that proposes these answers; its URL resolves once that change is merged to main.", + "two_person_review_status": "Unmet", + "two_person_review_justification": "Every human change is authored by the sole maintainer, and GitHub does not let an author approve their own pull request; the main ruleset requires zero approvals. Scorecard reported 0 of 27 approved changesets on 2026-10-11. A different person must review at least half of proposed modifications to change this.", + "build_reproducible_status": "Unmet", + "build_reproducible_justification": "Release archives and npm tarballs are byte-identical across rebuilds, but the .nupkg and the SBOM are not (see build_repeatable); the criterion requires a reproducible build for the project's results. Deterministic .nupkg and SBOM output would change this.", + "test_statement_coverage90_status": "Unmet", + "test_statement_coverage90_justification": "Statement coverage is not measured (see test_statement_coverage80).", + "test_branch_coverage80_status": "Unmet", + "test_branch_coverage80_justification": "Branch coverage is not measured (see test_statement_coverage80).", + "hardened_site_status": "Met", + "hardened_site_justification": "The repository, issue tracker and release downloads are served by github.com, which sent Strict-Transport-Security, Content-Security-Policy, X-Content-Type-Options: nosniff and X-Frame-Options: deny in responses to curl -I on 2026-10-11 for https://github.com/codegiveness/postgresql-sharp-mcp and the v0.5.0 release asset URL (the asset itself is then served from a GitHub-controlled redirect target that was not separately checked). The package registries are third-party sites: npmjs.com returned HSTS, a CSP, nosniff and X-Frame-Options; the nuget.org package page returned HSTS, nosniff and X-Frame-Options but its CSP was report-only.", + "general_comments": "Evidence-backed project metadata for https://www.bestpractices.dev/en/projects/15155 . Proposed answers require review and saving on the badge website. Silver and gold proposals were added on 2026-10-11 and cite files that exist only after the pull request that adds them is merged. Not inferred and therefore not proposed: require_2FA and secure_2FA (the GitHub account's 2FA method is not observable from the repository) and security_review (a human review of docs/assurance-case.md must be recorded in that document first). Also not inferred: developer knowledge beyond the owner's attestation; response timeliness for reports not yet received (0 issues, security advisories and Dependabot alerts and 0 open CodeQL alerts as checked through the GitHub API on 2026-10-10 and again on 2026-10-11); negotiated TLS key lengths, algorithms or forward secrecy beyond Npgsql, .NET and OS defaults and the operator's SSL Mode and PostgreSQL server settings; absence of vulnerabilities or credentials beyond the cited point-in-time scans; branch and statement coverage; and pre-release static analysis for v0.1.0, which predates the CodeQL workflow." } diff --git a/docs/security-posture.md b/docs/security-posture.md index 0e9db4c..5fa526d 100644 --- a/docs/security-posture.md +++ b/docs/security-posture.md @@ -21,6 +21,7 @@ See [SECURITY.md](../SECURITY.md) for credentials, TLS, extension privileges, re - CodeQL uses `security-extended` for C# and GitHub Actions. C# scans build the real solution to capture compiler-generated code. Trusted main/scheduled runs publish results. PR base/candidate scans and the aggregate gate use read-only tokens and preserve raw SARIF artifacts, without release secrets. For same-repository PRs only, a separate `pull-request-upload` job with `security-events: write` uploads the candidate SARIF that has already been produced. It never builds or executes PR code. This gives the PR head a code-scanning check run, which Scorecard's SAST check counts. Fork PRs remain read-only and get no upload. - The PR gate executes the comparator from the trusted base revision and rejects new CVSS >= 7/error-level findings, incomplete reports and extraction errors. Stable fingerprints and multiplicity prevent broad rule-only suppression. Reviewers must still inspect workflow changes; this is not protection against a maintainer deliberately weakening the workflow. - Dependency auditing checks direct and transitive NuGet packages against known advisories. Portable and per-platform lockfiles make dependency resolution explicit; locked restore rejects drift. GitHub Dependabot alerts and automatic security-update PRs are enabled; proposed fixes still require review and passing checks. +- Compiler warnings, including use of obsolete APIs, fail the build, and the CI `verify` job runs `dotnet format style --verify-no-changes --severity warn` against the repository's [`.editorconfig`](../.editorconfig) (unused usings, using placement, file-scoped namespaces, accessibility modifiers, naming). Layout is not enforced, and the check is a style gate, not a security scan ([coding standards](../CONTRIBUTING.md#coding-standards)). - OpenSSF Scorecard reports repository practices. Findings and the public score can lag configuration changes. The score is neither an independent vulnerability audit nor a certification. - Actions use verified full commit pins, checkout credentials are not persisted, and release configuration is scoped to the `release` environment. The local independent publisher changes remove npm publishing secrets in favor of stage-only OIDC; NuGet retains `NUGET_USERNAME` and its own matching policy. Repository policy enforces full-SHA Action pins and permits GitHub-owned Actions plus `ossf/scorecard-action` and `NuGet/login`; Marketplace-wide approval is disabled. The allowlist permits future SHA-pinned updates of those two Actions, rather than freezing Dependabot to one commit. No privileged `pull_request_target` or `workflow_run` execution of untrusted code is configured. - A `main` ruleset with no bypass actors requires PRs, up-to-date required checks from GitHub Actions and resolved review conversations, and blocks force pushes/deletion, including administrators; it replaced equivalent classic branch protection on 2026-10-10 (see [repository settings](git-workflow.md#branch-protection-on-main)). The explicitly chosen solo-maintainer policy requires zero independent approvals, no code-owner approval and no last-pusher approval. The maintainer can merge after required checks pass and resolve conversations themselves; this does not claim independent review. Default workflow tokens remain read-only, bot review approval remains disabled, and external fork workflows require owner approval before running. @@ -42,21 +43,48 @@ Results below are Scorecard v5.5.0 reports published to the [Scorecard API](http | `bde167e95e0fb3ff74b895db55c87f4b04d69b0c` | 2026-10-02 06:54:41 | 6.3 | | `b42a95d1a449d6363df93a3d4709a8178d2d3ced` (#30) | 2026-10-10 13:32:58 | 7.1, with Branch-Protection unmeasured (internal error) | | `8cd3166d2121249ce34863a80e9f063216659ed2` (#55, [run 38062959618](https://github.com/codegiveness/postgresql-sharp-mcp/actions/runs/38062959618)) | 2026-10-10 15:16:56 | 6.9, with Branch-Protection measured at 4 | +| `64c55c796adf75bff63b4af60a60edee9d8a492f` (#65, release 0.5.0 plus a docs fix; [run 38102103517](https://github.com/codegiveness/postgresql-sharp-mcp/actions/runs/38102103517)) | 2026-10-11 01:31:16 | 7.0 (SAST rose to 9; Signed-Releases rose to 1; Packaging became unmeasured) | The aggregate fell because Branch-Protection, a high-weight check previously excluded as an error, is now measured. No check measured in both 2026-10-10 reports went down. Checks below 10 in the 15:16:56 report: | Check | 13:32 → 15:16 | What v5.5.0 measures | Status and what would change it | |---|---|---|---| | Branch-Protection | error → 4 | Tiers that cap the score until complete: no deletion/force push (3); ≥1 required approver, PRs, up-to-date branches, last-push approval (3); status checks (2); ≥2 approvers and code-owner review (1); administrator enforcement and stale-review dismissal (1) | `main` now uses a ruleset, readable by the default token, so no administration-read token is stored. Tier 1 is complete; tier 2 lacks a required approver and last-push approval, and either would block a sole maintainer's own merges. 4 is the ceiling until a second person can review. | -| Signed-Releases | 0 → 0 | Assets of the last five releases: an `.intoto.jsonl` asset scores 10 per release; a signature asset (`.sigstore.json`, `.sig`, `.asc`, `.minisig`, `.sign`, `.sigstore`) scores 8; the result is the average | Every release from v0.2.0 has a Sigstore-signed SLSA attestation, but only in GitHub's attestation store. From the next release, `provenance.sigstore.json` is published (8 for that release, so the average rises by about 1.6 per release to 8). Attaching the existing bundles to v0.2.0–v0.4.0 is a release edit reserved for the owner. 10 would need `.intoto.jsonl` provenance; the GitHub bundle is not in that format, and the SLSA generator's reusable workflow must be referenced by tag, which conflicts with the full-SHA pin policy and Actions allowlist. | +| Signed-Releases | 0 → 0 | Assets of the last five releases: an `.intoto.jsonl` asset scores 10 per release; a signature asset (`.sigstore.json`, `.sig`, `.asc`, `.minisig`, `.sign`, `.sigstore`) scores 8; the result is the average | Every release from v0.2.0 has a Sigstore-signed SLSA attestation, but only in GitHub's attestation store. From the next release, `provenance.sigstore.json` is published (8 for that release, so the average rises by about 1.6 per release to 8). Attaching the existing bundles to older releases is a release edit reserved for the owner (see the [owner action checklist](#owner-action-checklist)). 10 would need `.intoto.jsonl` provenance; the GitHub bundle is not in that format, and the SLSA generator's reusable workflow must be referenced by tag, which conflicts with the full-SHA pin policy and Actions allowlist. | | Code-Review | 0 → 0 (0/15 → 0/16) | Recent default-branch changesets approved by someone other than their author. An approved bot changeset is removed from the total, not counted as approved | Every human changeset is owner-authored, and GitHub does not let the owner approve them. Reviewing and approving Dependabot PRs is still worth doing, but under v5.5.0 it only removes those changesets from the total and cannot lift the score above 0. That needs a different person approving the owner's PRs, or the owner approving PRs from outside contributors. | | Contributors | 0 → 0 | Distinct organizations/companies among contributors with at least 5 commits; 3 for 10 | One maintainer with no affiliation listed. Changes only with contributors from other organizations. | | Maintained | 0 → 0 | 0 while the repository is under 90 days old; then commits and issue activity over 90 days, one per week for 10 | Created 2026-10-02 02:18:32 UTC; age-eligible from 2026-12-31 02:18:32 UTC. Extra commits or issues cannot advance this. | -| CII-Best-Practices | 2 → 5 | Best Practices badge level: in progress 2, passing 5, silver 7, gold 10 | The owner saved the passing answers on 2026-10-10 (see [enrollment](#openssf-best-practices-enrollment)). Silver needs the remaining silver MUST criteria answered, including `access_continuity`; gold requires `two_person_review` and two unassociated significant contributors. | +| CII-Best-Practices | 2 → 5 | Best Practices badge level: in progress 2, passing 5, silver 7, gold 10 | The owner saved the passing answers on 2026-10-10 (see [enrollment](#openssf-best-practices-enrollment)). Silver needs every silver MUST criterion Met; five are Unmet today. Gold also requires `two_person_review` and two unassociated significant contributors ([what stays unmet](#what-stays-unmet-and-what-would-change-it)). | | SAST | 7 → 7 (3/30 → 7/30 commits) | CodeQL configured (weight 7) plus the share of the last 30 commits whose merged PR head has a successful code-scanning check run (weight 3) | Every PR merged since #29 counts, including Dependabot PRs. Older commits leave the sample only as real PRs merge; 10 needs all 30 sampled commits checked. | Scored 10 in both reports: Binary-Artifacts, CI-Tests, Dangerous-Workflow, Dependency-Update-Tool, Fuzzing, License, Packaging, Pinned-Dependencies, Security-Policy, Token-Permissions and Vulnerabilities. Fuzzing is detected through the FsCheck properties (`tools/PostgreSqlMcp.Verify/FuzzChecks.cs`), not the SharpFuzz campaign. No ClusterFuzzLite integration was added: it needs a Bash build script, which this repository's C#-only rule excludes, and would rerun the existing SharpFuzz harness. Token-Permissions still warns about `contents: write` in the GitHub Release job, which needs it to publish. These are detector results, not a comprehensive workflow audit. +#### Scorecard on 2026-10-11, and what a Gold project scores + +The [report for `64c55c7`](https://api.securityscorecards.dev/projects/github.com/codegiveness/postgresql-sharp-mcp) (generated 2026-10-11 01:31:16 UTC by Scorecard v5.5.0 commit `c395761`) gives an aggregate of **7.0**. Checks below 10 and what would change each: + +| Check | 2026-10-10 15:16 → 2026-10-11 01:31 | Scorecard's reason | What would change it | +|---|---|---|---| +| Branch-Protection | 4 → 4 | Branch `main` does not require approvers, code-owner review or last-push approval; the other settings pass | A second human who can approve the maintainer's pull requests, and turning on required approvals, code-owner review and last-push approval for them. Without that person each of those settings would block the maintainer's own merges, so 4 stays. | +| Code-Review | 0 → 0 | 0 of 27 approved changesets | A different person approving the maintainer's pull requests, or the maintainer approving pull requests from outside contributors. Dependabot approvals only shrink the denominator. | +| Contributors | 0 → 0 | 0 contributing organizations | Contributors with at least five commits each from at least three organizations. | +| Maintained | 0 → 0 | Repository created within the last 90 days | Time. Age-eligible from 2026-12-31 02:18:32 UTC, then sustained commit and issue activity (10 needs about one per week) gives 10; artificial activity is not used. | +| CII-Best-Practices | 5 → 5 | Badge detected: Passing | Silver scores 7 and Gold 10. See [what stays unmet](#what-stays-unmet-and-what-would-change-it). | +| Signed-Releases | 0 → 1 | Only v0.5.0 has a signed artifact (`provenance.sigstore.json`); v0.3.1 to v0.4.0 have none | Release 0.5.0 is observed to carry `provenance.sigstore.json` (release assets read on 2026-10-11). Each of the four older releases in Scorecard's window that receives its existing bundle adds 8/5 to the score, and each new release replaces the oldest; five signed releases score 8. 10 needs `.intoto.jsonl` provenance, which conflicts with the full-SHA pin policy ([above](#published-scorecard-findings)). | +| SAST | 7 → 9 | 25 of the last 30 commits are checked | Time and normal merges: five older commits leave the sample as new PRs merge. | +| Packaging | 10 → −1 | No GitHub/GitLab publishing workflow detected | Not a loss: −1 means the check is inconclusive and is excluded from the aggregate. Scorecard does not say why detection changed; the change coincides with 0.5.0 moving NuGet publication from an inline `dotnet nuget push` to the compiled release tool [INFERENCE]. The workflow is not reshaped to satisfy the heuristic. | + +Scored 10: Binary-Artifacts, CI-Tests, Dangerous-Workflow, Dependency-Update-Tool, Fuzzing, License, Pinned-Dependencies, Security-Policy, Token-Permissions (with a recorded warning for `contents: write` in the GitHub Release job) and Vulnerabilities. Open code-scanning alerts on 2026-10-11 were four Scorecard practice alerts (Branch-Protection, Maintained, Code-Review, SAST); CodeQL had none. + +The reference project, [ossf/best-practices-badge](https://github.com/ossf/best-practices-badge) (the application behind the badge), was observed on 2026-10-11: + +- [Its Best Practices entry (project 1)](https://www.bestpractices.dev/projects/1.json) reported `badge_level: gold`, with passing, silver and gold all achieved on 2023-09-19 and the entry last updated 2026-07-18. +- [Its Scorecard report](https://api.securityscorecards.dev/projects/github.com/ossf/best-practices-badge) was generated 2026-10-09 02:01:08 UTC (Scorecard v5.5.0, commit `1059cb3`) with an aggregate of **8.2**: Maintained, Dangerous-Workflow, Dependency-Update-Tool, Security-Policy, Token-Permissions, Binary-Artifacts, CII-Best-Practices (Gold), Vulnerabilities, Signed-Releases, SAST, License, CI-Tests and Contributors (35 organizations) at 10; Pinned-Dependencies 9; Branch-Protection 3; Code-Review 1 (1 of 10 changesets approved); Fuzzing 0; Packaging −1. + +So a Gold badge does not require a perfect Scorecard: that project also scores low on Code-Review, Branch-Protection and Fuzzing. It reaches Signed-Releases 10 by publishing a Sigstore signature and SLSA `.intoto.jsonl` provenance for its SBOM release assets, the latter produced by the SLSA generator's tag-referenced reusable workflow, which this repository's pin policy rejects. + +[INFERENCE] Scorecard weights each check by risk (critical 10, high 7.5, medium 5, low 2.5; [check documentation](https://github.com/ossf/scorecard/blob/main/docs/checks.md)). Recomputing with those weights reproduced both reported aggregates within rounding (6.95 against 7.0 and 8.25 against 8.2). Applying them, this repository would reach about 8.3 with no second person involved if Maintained rose to 10 after 2026-12-31, SAST to 10 and Signed-Releases to 8, which is where the reference project stands overall. This is an estimate of the formula's output, not a prediction of future reports. + The sibling [mssql-mcp report](https://api.securityscorecards.dev/projects/github.com/codegiveness/mssql-mcp), dated 2026-10-02 05:37:07 UTC, scored 6.4 and also reported Code-Review 0, Maintained 0, Fuzzing 0 and SAST 7. Its live branch policy requires zero approvals, and PR 149 had no review entries. Dependabot and Release Please create useful update/release PRs; they do not supply independent human approval. This repository retains the useful PR/check, dependency-update and pinned-Action patterns without copying group-review requirements, Bash launchers or unrelated registry credentials. A solo workflow can be well-tested and secure without receiving every Scorecard practice score. For the 2026-10-02 assessment, local verification used SDK 10.0.401 with locked restores. The final Release solution build passed with zero warnings/errors. Disposable PostgreSQL integration passed all runtime scenarios, including 129 FsCheck literal properties, permissions/RLS, target isolation, rollback, result limits, confidentiality, HypoPG cleanup and the SARIF comparator regressions. A nominal 10-second native campaign replayed 13 seeds and completed 18,975 engine runs in 12 seconds with an empty crash directory; `campaign 0` rejected the invalid duration with exit 1. The first campaign exposed a nonportable `nul.sql` seed name on the shared filesystem; renaming it to `null-byte.sql` preserved its bytes and allowed replay. The verifier also initially failed compilation due to a missing `async` modifier; the same integration entrypoint passed after correction. These finite local results do not establish hosted workflow success, exhaustive coverage or a vulnerability-free runtime. @@ -68,7 +96,7 @@ From release 0.2.0, the release workflow generates: 1. Five self-contained platform archives, npm tarballs (the main package and five platform packages, each platform package holding one self-contained runtime) and a NuGet tool package. 2. An application CycloneDX JSON SBOM generated by the pinned .NET CycloneDX tool from the resolved NuGet graph, excluding dependencies classified as development-only. Some build-time transitive packages may remain; this is not a complete operating-system/container/native-library inventory. 3. A `SHA256SUMS` manifest covering the five archives, npm/NuGet packages and SBOM. Checksums detect mismatched bytes; an unauthenticated checksum download is not proof of publisher identity. -4. Requested GitHub build attestations for the archives, npm/NuGet package files, SBOM and checksum manifest. Verify the subject digest and repository/workflow identity; do not infer attestation success from workflow configuration. The attested NuGet subject is the GitHub asset, not a claim that registry signing preserves those bytes. Releases dispatched after the provenance-bundle change reached `main` also carry that attestation's Sigstore bundle as the `provenance.sigstore.json` GitHub Release asset; the release tool refuses to publish unless the bundle is one SLSA v1 provenance statement whose subjects are exactly the uploaded assets and their SHA-256 digests. Releases up to v0.4.0 have attestations in GitHub's store but no bundle asset. +4. Requested GitHub build attestations for the archives, npm/NuGet package files, SBOM and checksum manifest. Verify the subject digest and repository/workflow identity; do not infer attestation success from workflow configuration. The attested NuGet subject is the GitHub asset, not a claim that registry signing preserves those bytes. Releases dispatched after the provenance-bundle change reached `main` also carry that attestation's Sigstore bundle as the `provenance.sigstore.json` GitHub Release asset; the release tool refuses to publish unless the bundle is one SLSA v1 provenance statement whose subjects are exactly the uploaded assets and their SHA-256 digests. Releases up to v0.4.0 have attestations in GitHub's store but no bundle asset. Release 0.5.0, published 2026-10-11 01:12:33 UTC, lists `provenance.sigstore.json` among its release assets (read through `gh release view v0.5.0` on 2026-10-11). 5. Requested npm provenance for a successful registry publication. NuGet trusted publishing exchanges GitHub OIDC for a short-lived API key; this authenticates publication and is not equivalent to npm provenance. ### Observed 0.3.3 release @@ -158,6 +186,65 @@ The owner chose on 2026-10-10 to answer the criteria previously left open. Devel The repository was created on 2026-10-02; activity does not supply months of maintenance or response history. Independent review also remains distinct from the enforced PR/check policy. +#### Silver and Gold proposals (2026-10-11) + +On 2026-10-11 the [project JSON](https://www.bestpractices.dev/projects/15155.json) still reported `badge_level: passing`, passing 100%, silver 15%, gold 22% and `updated_at` 2026-10-10 13:37:58 UTC, so no silver or gold answer has been saved. The criteria are listed at [silver](https://www.bestpractices.dev/en/criteria/1) and [gold](https://www.bestpractices.dev/en/criteria/2); several criteria appear at both levels and share one answer field, so a single answer applies to both. + +[`.bestpractices.json`](../.bestpractices.json) now also proposes answers for 57 silver and gold criteria: 36 Met, 17 Unmet and 4 N/A. Each Met answer cites a file, a command result or a dated API observation; files added by the same pull request resolve only after it merges. The proposals are not a certification, and nothing is saved on the hosted entry until the owner reviews each answer and saves it. Not proposed, because the repository cannot establish them: + +- `require_2FA` and `secure_2FA`: whether the maintainer's GitHub account uses two-factor authentication, and with which method, is not observable from the repository (the API returned no value for the authenticated account on 2026-10-11). GitHub began requiring 2FA from code contributors on github.com in 2023 ([GitHub announcement](https://github.blog/news-insights/product-news/raising-the-bar-for-software-security-github-2fa-begins-march-13/)), but the answer is the owner's attestation. +- `security_review`: the criterion needs a human review of the security requirements and boundary. [docs/assurance-case.md](assurance-case.md#review-record) is a draft by an AI agent and has a pending maintainer row; record the review there first. + +Silver still fails on five MUST criteria (`access_continuity`, `build_repeatable`, `test_statement_coverage80`, `crypto_certificate_verification`, `crypto_verification_private`), so `achieve_silver` and the Scorecard CII-Best-Practices score stay at passing until they change. Gold additionally needs the Unmet criteria in the next table. The silver and gold answers describe 2026-10-11 and must be re-evaluated when reports, releases or settings change. + +#### What stays unmet and what would change it + +| Criterion or check | Level | Why it is unmet today | Exactly what would change it | +|---|---|---|---| +| `access_continuity` | Silver MUST | One account holds repository admin, npm and NuGet authority | A second person with admin, npm maintainer and NuGet ownership able to merge, release and publish within a week, and a recorded handover test ([GOVERNANCE.md](../GOVERNANCE.md#continuity-if-the-maintainer-is-unavailable)) | +| `bus_factor` | Silver SHOULD, Gold MUST | One human contributor (58 commits and Dependabot's 3 on 2026-10-11) | At least two people who can each maintain the project, which the same second maintainer provides | +| `contributors_unassociated`; Scorecard Contributors | Gold MUST; check | No second contributor | At least two significant contributors not paid by the same organization; Scorecard also needs people from other organizations (three organizations scores 10) | +| `two_person_review`; Scorecard Code-Review | Gold MUST; check | Every human change is authored by the maintainer and GitHub blocks self-approval; Scorecard found 0 of 27 changesets approved | A different human reviewing and approving at least half of proposed modifications, or the maintainer approving outside contributors' PRs. Bot or agent approvals do not count | +| Scorecard Branch-Protection above 4 | Check | Required approvers, code-owner review and last-push approval are off because they would block a sole maintainer | Enable them only once a second reviewer exists | +| Scorecard Maintained | Check | Repository younger than 90 days | Time: age-eligible 2026-12-31, then normal activity | +| Scorecard Signed-Releases above 8 | Check | The GitHub bundle is not `.intoto.jsonl` | A tag-referenced SLSA generator, rejected by the full-SHA pin policy. Reaching 8 needs bundles on the five newest releases | +| `test_statement_coverage80`, `test_statement_coverage90`, `test_branch_coverage80` | Silver MUST, Gold MUST | No coverage is measured | Run a FLOSS .NET coverage collector over the integration run in CI, publish the figures, and add tests until they reach 80% statement, 90% statement and 80% branch | +| `build_repeatable`, `build_reproducible` | Silver MUST, Gold MUST | `.nupkg` zip timestamps and the SBOM's random serial number and timestamp differ per build | Deterministic `.nupkg` and SBOM output, verified by comparing two rebuilds in `release-regressions` | +| `crypto_certificate_verification`, `crypto_verification_private`, `crypto_used_network` | Silver MUST, MUST, SHOULD (Gold MUST for the last) | Npgsql's default `SSL Mode=Prefer` does not verify the server and can fall back to plaintext; the server only warns | A behavior change, with an upgrade note, so non-loopback connections require verified TLS unless the operator opts out | +| `version_tags_signed` | Silver SUGGESTED | Tags are annotated but unsigned (v0.3.3 is lightweight) | Sign release tags (`git tag -s` with a signing key registered on GitHub) and document `git tag -v` | +| `small_tasks` | Gold MUST | No open issue carries `good first issue` or `help wanted` | Label real, small issues | +| `dco` | Silver SHOULD | No sign-off policy | Adopt and enforce a DCO or contributor agreement | +| `internationalization` | Silver SHOULD | English-only output | Externalize user-facing strings; not planned | +| `build_preserve_debug` | Silver SHOULD | Release packaging forces `DebugType=None` | Honor the caller's debug-information flags in release packaging | +| `security_review`, `require_2FA`, `secure_2FA` | Gold MUST, MUST, SHOULD | Owner-only facts, not yet answered | See the checklist below | + +### Owner action checklist + +Nothing here has been done by the change that proposes it; each item changes a remote setting or a published release and needs the owner. + +1. **Review and merge the pull request** that adds `GOVERNANCE.md`, `docs/architecture.md`, `docs/roadmap.md`, `docs/assurance-case.md`, the coding and review standards, the SPDX headers, `.editorconfig` with its CI step and the answer proposals. The proposal evidence URLs resolve only after the merge. +2. **Read `docs/assurance-case.md` and record your review** in its review record. Confirm or correct the roadmap and governance statements, which are drafts written for the maintainer's approval. +3. **Save the answers on bestpractices.dev.** Sign in as the owner and open the [silver](https://www.bestpractices.dev/en/projects/15155/silver/edit) and [gold](https://www.bestpractices.dev/en/projects/15155/gold/edit) sections. Use **Save (and continue) 🤖** so the site loads the repository proposals from `.bestpractices.json`, review every proposed answer against the evidence, and change any you disagree with. Answer `require_2FA`, `secure_2FA` and `security_review` yourself. +4. **Attach the existing provenance bundles to the older releases in Scorecard's window** (v0.3.1, v0.3.2, v0.3.3 and v0.4.0; v0.2.0 is already outside the window, and v0.1.0 has no attestation). Editing a published release is reserved for the owner. On 2026-10-11 the read-only first steps below were run for v0.4.0: its attestation was retrieved as one Sigstore bundle holding one SLSA v1 statement with nine subjects. The verification and upload steps were not run, and the `gh` version used there has no `gh attestation` command, so use a current one: + + ```bash + mkdir v0.4.0 && cd v0.4.0 + gh release download v0.4.0 --repo codegiveness/postgresql-sharp-mcp --pattern SHA256SUMS + D=$(sha256sum SHA256SUMS | cut -d' ' -f1) + gh api "repos/codegiveness/postgresql-sharp-mcp/attestations/sha256:$D" --jq '.attestations[0].bundle' > provenance.sigstore.json + gh attestation verify SHA256SUMS --repo codegiveness/postgresql-sharp-mcp \ + --signer-workflow codegiveness/postgresql-sharp-mcp/.github/workflows/release.yml \ + --bundle provenance.sigstore.json + gh release upload v0.4.0 provenance.sigstore.json --repo codegiveness/postgresql-sharp-mcp + ``` + + Upload only after verification passes, and repeat for the other three tags. Check that the bundle's subjects match that release's asset digests first. +5. **Label real small tasks.** Open or label two or three small issues `good first issue` (for example, add a direct `SqlGuard` verifier scenario, make constructor style consistent in `DatabaseTools`, or document the `ConfigureAwait` convention). This is what `small_tasks` asks for. +6. **Decide the TLS default.** Whether non-loopback connections should require verified TLS unless the operator opts out is a product decision with an upgrade note; it is the change behind three Unmet crypto criteria. +7. **Optional: sign release tags** with a signing key registered on GitHub, which would settle `version_tags_signed`. +8. **Optional: recruit a second maintainer** and follow the continuity steps. This is the only path to `access_continuity`, `bus_factor`, `two_person_review`, Code-Review, Contributors and a higher Branch-Protection score. No artificial reviews or contributors are used. +9. **Check the settings you own.** GitHub's secret-scanning generic patterns and validity checks were still reported disabled on 2026-10-11 ([enrollment](#openssf-best-practices-enrollment) and the 2026-10-02 note above); enabling them is a settings change for the owner. + ## Sources - [GitHub secure use of Actions](https://docs.github.com/en/actions/reference/security/secure-use) @@ -181,3 +268,7 @@ The repository was created on 2026-10-02; activity does not supply months of mai - [NuGet trusted publishing](https://learn.microsoft.com/nuget/nuget-org/trusted-publishing) - [PostgreSQL read-only transactions](https://www.postgresql.org/docs/current/sql-set-transaction.html) - [GitHub profile reference](https://docs.github.com/en/account-and-profile/reference/profile-reference#earning-achievements) +- [OpenSSF Best Practices silver criteria](https://www.bestpractices.dev/en/criteria/1) and [gold criteria](https://www.bestpractices.dev/en/criteria/2) +- [OpenSSF Scorecard checks and risk levels](https://github.com/ossf/scorecard/blob/main/docs/checks.md) +- [Npgsql encryption and SSL Mode](https://www.npgsql.org/doc/security.html) +- [OpenSSF Best Practices badge application](https://github.com/ossf/best-practices-badge) (reference project) From cbe8e83b35126c30c1c7710011040fc80acc350a Mon Sep 17 00:00:00 2001 From: AG <288536783+codegiveness@users.noreply.github.com> Date: Sun, 11 Oct 2026 09:27:39 +0700 Subject: [PATCH 04/12] docs: document the single-maintainer lockbox route to access continuity The OpenSSF access_continuity criterion explicitly allows an individual maintainer to meet it with a lockbox and a will. GOVERNANCE.md, the security posture and the proposed answer now describe both routes instead of only a second maintainer. --- .bestpractices.json | 2 +- GOVERNANCE.md | 23 ++++++++++++++++------- docs/security-posture.md | 7 ++++--- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/.bestpractices.json b/.bestpractices.json index 8df23d9..9a10d6d 100644 --- a/.bestpractices.json +++ b/.bestpractices.json @@ -142,7 +142,7 @@ "roles_responsibilities_status": "Met", "roles_responsibilities_justification": "GOVERNANCE.md defines the Maintainer, Contributor, Reviewer, Security reporter, Automation and AI-agent roles with their responsibilities and says who holds each (the maintainer role is held by the GitHub user codegiveness): https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/GOVERNANCE.md#roles-and-responsibilities . This file is added by the pull request that proposes these answers; its URL resolves once that change is merged to main.", "access_continuity_status": "Unmet", - "access_continuity_justification": "Not met on 2026-10-11. No second person has the access needed to merge, release or publish if the maintainer is unavailable; GitHub admin, npm package maintainer and NuGet package ownership rest with one account. What exists (public MIT source, written release procedure, no stored credentials, verifiable artifacts) and exactly what would change the answer are recorded in https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/GOVERNANCE.md#continuity-if-the-maintainer-is-unavailable . This file is added by the pull request that proposes these answers; its URL resolves once that change is merged to main.", + "access_continuity_justification": "Not met on 2026-10-11. Nobody but the maintainer can currently merge, release or publish: GitHub admin, npm package maintainer and NuGet package ownership rest with one account, and no lockbox arrangement is recorded. What exists (public MIT source, written release procedure, no stored credentials, verifiable artifacts) and the two routes that would change the answer (a second maintainer, or the criterion's lockbox-and-will route for an individual maintainer) are recorded in https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/GOVERNANCE.md#continuity-if-the-maintainer-is-unavailable . This file is added by the pull request that proposes these answers; its URL resolves once that change is merged to main.", "bus_factor_status": "Unmet", "bus_factor_justification": "The bus factor is 1: the GitHub contributors API reported one human contributor (58 commits) and Dependabot (3) on 2026-10-11. See https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/GOVERNANCE.md#continuity-if-the-maintainer-is-unavailable for the steps that would raise it. This file is added by the pull request that proposes these answers; its URL resolves once that change is merged to main.", "documentation_roadmap_status": "Met", diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 8401399..968ad85 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -38,7 +38,7 @@ There is no quota and no automatic promotion. The maintainer may invite a contri ## Continuity if the maintainer is unavailable -**Status on 2026-10-11: the access-continuity requirement is not met, and the bus factor is 1.** No second person has the access needed to continue the project. This section records what that means so that users can judge the risk, and what exactly would change it. +**Status on 2026-10-11: the access-continuity requirement is not met, and the bus factor is 1.** Nobody else can currently use the access needed to continue the project. This section records what that means so that users can judge the risk, and what exactly would change it. ### What depends on the maintainer alone @@ -64,14 +64,23 @@ These measures preserve users' ability to verify and fork. They do not let anyon ### What would change the status -The requirement is met only when a second trusted person actually holds these permissions and could use them without the maintainer. Documentation alone does not meet it. Concretely: +The OpenSSF criterion requires that issues can be created and closed, changes accepted and versions released within a week of confirmed loss of any one person. It names two ways to do that: someone else holds the necessary keys, passwords and legal rights, or, for a project run by one individual, the keys sit in a lockbox and a will grants the needed legal rights. Documentation alone meets neither. Either route below would change `access_continuity` to Met. -1. A second person with **admin** on `codegiveness/postgresql-sharp-mcp` and 2FA on their GitHub account, able to merge, edit the ruleset, dispatch `release.yml` and manage the `release` environment. -2. The same person added as a **maintainer of the six `@codegiveness/...` npm packages** (2FA required), able to approve staged versions, and able to edit the trusted-publisher configuration. -3. The same person as **co-owner of the NuGet package** with their own trusted-publishing policy for `release.yml` and the `release` environment, and their own `NUGET_USERNAME`. -4. A tested handover: the second person completes one real issue-to-release cycle (or a dry run of every step the maintainer can verify), and the maintainer records the date and result in this document. +**Route 1: a second maintainer.** This also changes the bus factor. -When all four hold, update this section and the OpenSSF Best Practices answers for `access_continuity` and `bus_factor` ([security posture](docs/security-posture.md#openssf-best-practices-enrollment)). Until then they stay Unmet. +1. A second person has **admin** on `codegiveness/postgresql-sharp-mcp` and 2FA on their GitHub account. They can merge, edit the ruleset, dispatch `release.yml` and manage the `release` environment. +2. The same person is a **maintainer of the six `@codegiveness/...` npm packages** (2FA required). They can approve staged versions and edit the trusted-publisher configuration. +3. The same person is a **co-owner of the NuGet package**, with their own trusted-publishing policy for `release.yml` and the `release` environment, and their own `NUGET_USERNAME`. +4. The handover is tested. The second person completes one real issue-to-release cycle, or a dry run of every step the maintainer can verify. The maintainer records the date and result in this document. + +**Route 2: lockbox and written authority, for a single maintainer.** This does not change the bus factor. + +1. A named trusted person has sealed access to the recovery material for the maintainer's GitHub, npm and NuGet accounts. That material includes the passwords and the 2FA recovery codes or a backup security key, and it must be enough to complete the [release procedure](CONTRIBUTING.md#independent-publisher-setup), including npm's 2FA approval. A password manager's emergency-access feature or a sealed physical copy both work. Credentials never go into this repository, an issue or a pull request. +2. A will or signed letter gives that person the right to use the material to continue the project or hand it to a new maintainer, and says when they may do so. +3. Optionally, GitHub's [account successor](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/repository-access-and-collaboration/maintaining-ownership-continuity-of-your-personal-accounts-repositories) is set as a backstop. It only lets the successor archive or transfer public repositories, after GitHub's waiting period, so it covers neither npm, NuGet, nor incapacity short of death. It does not replace step 1. +4. The maintainer records in this document that the arrangement exists and when the material was last checked to be current. The trusted person's private details appear only if they agree. The check is repeated at least yearly and whenever a password, recovery code or 2FA device changes. + +When either route is complete, update this section and the OpenSSF Best Practices answer for `access_continuity`; route 1 also updates `bus_factor` ([security posture](docs/security-posture.md#openssf-best-practices-enrollment)). Until then both stay Unmet. ## Related documents diff --git a/docs/security-posture.md b/docs/security-posture.md index 5fa526d..c0b51c4 100644 --- a/docs/security-posture.md +++ b/docs/security-posture.md @@ -201,7 +201,7 @@ Silver still fails on five MUST criteria (`access_continuity`, `build_repeatable | Criterion or check | Level | Why it is unmet today | Exactly what would change it | |---|---|---|---| -| `access_continuity` | Silver MUST | One account holds repository admin, npm and NuGet authority | A second person with admin, npm maintainer and NuGet ownership able to merge, release and publish within a week, and a recorded handover test ([GOVERNANCE.md](../GOVERNANCE.md#continuity-if-the-maintainer-is-unavailable)) | +| `access_continuity` | Silver MUST | One account holds repository admin, npm and NuGet authority, and nobody else can use it | Either a second person with admin, npm maintainer and NuGet ownership who can merge, release and publish within a week, plus a recorded handover test; or, for a single maintainer as the criterion allows, a named trusted person holding sealed recovery material for the GitHub, npm and NuGet accounts with written authority to use it, recorded and checked yearly ([GOVERNANCE.md](../GOVERNANCE.md#what-would-change-the-status)) | | `bus_factor` | Silver SHOULD, Gold MUST | One human contributor (58 commits and Dependabot's 3 on 2026-10-11) | At least two people who can each maintain the project, which the same second maintainer provides | | `contributors_unassociated`; Scorecard Contributors | Gold MUST; check | No second contributor | At least two significant contributors not paid by the same organization; Scorecard also needs people from other organizations (three organizations scores 10) | | `two_person_review`; Scorecard Code-Review | Gold MUST; check | Every human change is authored by the maintainer and GitHub blocks self-approval; Scorecard found 0 of 27 changesets approved | A different human reviewing and approving at least half of proposed modifications, or the maintainer approving outside contributors' PRs. Bot or agent approvals do not count | @@ -242,8 +242,9 @@ Nothing here has been done by the change that proposes it; each item changes a r 5. **Label real small tasks.** Open or label two or three small issues `good first issue` (for example, add a direct `SqlGuard` verifier scenario, make constructor style consistent in `DatabaseTools`, or document the `ConfigureAwait` convention). This is what `small_tasks` asks for. 6. **Decide the TLS default.** Whether non-loopback connections should require verified TLS unless the operator opts out is a product decision with an upgrade note; it is the change behind three Unmet crypto criteria. 7. **Optional: sign release tags** with a signing key registered on GitHub, which would settle `version_tags_signed`. -8. **Optional: recruit a second maintainer** and follow the continuity steps. This is the only path to `access_continuity`, `bus_factor`, `two_person_review`, Code-Review, Contributors and a higher Branch-Protection score. No artificial reviews or contributors are used. -9. **Check the settings you own.** GitHub's secret-scanning generic patterns and validity checks were still reported disabled on 2026-10-11 ([enrollment](#openssf-best-practices-enrollment) and the 2026-10-02 note above); enabling them is a settings change for the owner. +8. **Set up access continuity.** Route 2 in [GOVERNANCE.md](../GOVERNANCE.md#what-would-change-the-status) needs no second maintainer. Give a named trusted person sealed recovery material for the GitHub, npm and NuGet accounts and written authority to use it, then record the date in GOVERNANCE.md and change `access_continuity` to Met with that evidence. Never put the material in the repository. Optionally also set a GitHub account successor. +9. **Optional: recruit a second maintainer** (route 1). This is the only path to `bus_factor`, `two_person_review`, Code-Review, Contributors and a higher Branch-Protection score. No artificial reviews or contributors are used. +10. **Check the settings you own.** GitHub's secret-scanning generic patterns and validity checks were still reported disabled on 2026-10-11 ([enrollment](#openssf-best-practices-enrollment) and the 2026-10-02 note above); enabling them is a settings change for the owner. ## Sources From 095f0732e382096c22063e64cd7aed7310293b5b Mon Sep 17 00:00:00 2001 From: AG <288536783+codegiveness@users.noreply.github.com> Date: Sun, 11 Oct 2026 09:50:38 +0700 Subject: [PATCH 05/12] build: make the .nupkg and CycloneDX SBOM reproducible Pass NuGet's DeterministicTimestamp (1980-01-01) to dotnet pack so no .nupkg entry carries the wall clock, and normalize the SBOM serialNumber and metadata.timestamp in release-tool sbom (content-derived UUID, commit time). release-regressions proves both offline and packages rejects stamped entries. --- CHANGELOG.md | 1 + CONTRIBUTING.md | 2 +- tools/PostgreSqlMcp.Build/Program.cs | 5 +- tools/PostgreSqlMcp.Release/Program.cs | 9 ++- tools/PostgreSqlMcp.Release/SbomNormalizer.cs | 39 ++++++++++++ tools/PostgreSqlMcp.Verify/Packages.cs | 1 + .../PostgreSqlMcp.Verify.csproj | 1 + .../ReleaseRegressions.cs | 61 +++++++++++++++++++ 8 files changed, 116 insertions(+), 3 deletions(-) create mode 100644 tools/PostgreSqlMcp.Release/SbomNormalizer.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 91c4342..e85f8e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Documentation, policy and tooling only; no runtime behavior change. - CONTRIBUTING: add coding standards, code-review standards and small-task guidance. Add an `.editorconfig` and a CI step (`dotnet format style --verify-no-changes --severity warn`) that enforces the C# style rules. Source files gain SPDX copyright and license headers, and unused `using` directives are removed. - SECURITY: document how vulnerability reports are handled. - Propose Silver and Gold Best Practices answers in `.bestpractices.json`, and record the live Scorecard figures, the reference project's figures, the criteria that remain unmet and an owner checklist in [docs/security-posture.md](docs/security-posture.md). Nothing is saved on bestpractices.dev until the owner reviews it. +- Make the `.nupkg` and the CycloneDX SBOM reproducible, so every release asset except the per-run provenance bundle is byte-identical across rebuilds of the same source, and `SHA256SUMS` with them. `tools/PostgreSqlMcp.Build` passes NuGet's supported `DeterministicTimestamp` property to `dotnet pack`, fixing every zip entry time at 1980-01-01 (previously the wall clock; the entry order, contents and `nuget.psmdcp` name were already stable, so published package layouts are unchanged). `release-tool sbom` rewrites dotnet-CycloneDX's random `serialNumber` and wall-clock `metadata.timestamp` (the generator can only omit the serial number and cannot fix the timestamp) to a version 8 UUID derived from the SHA-256 of the BOM and the checked-out commit's time; the BOM stays valid CycloneDX JSON with the same properties and component order. `release-regressions` now proves both offline (SBOM normalization and two `dotnet pack` runs of a probe project more than two seconds apart), and `packages` rejects a `.nupkg` entry that carries a build-time stamp. The new `.nupkg` entry timestamps and SBOM fields apply to the next release; already published versions are unchanged. ## 0.5.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 43e31cb..e6bd76f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -260,7 +260,7 @@ Compatible fixes use a patch increment, such as **0.3.2 → 0.3.3**; reserve a m - **CI** runs database integration and package installation checks for pushes and pull requests and uploads generated packages for inspection. - **Release workflow** is manually dispatched from `main` for an existing `v` tag reachable from `origin/main`; pushing a tag does not start publication. Read-only preflight compiles trusted release tooling from the immutable main workflow revision and validates the tag's commit. Read-only packaging checks out that commit and verifies version consistency and behavior. Checkout-free attestation and publishing jobs consume immutable artifact IDs with digest mismatches rejected; they do not compile or execute tag-source release tooling. npm stages through package-specific GitHub OIDC, without `NPM_TOKEN` or token fallback, and waits for Windows 2FA approval; NuGet uses its own matching OIDC policy with `NUGET_USERNAME`. Artifact creation, staging or a successful GitHub release alone does not prove registry publication. - **Release re-runs never replace published bytes.** The GitHub job uploads only assets missing from the release (no `--clobber`). An existing asset, including `SHA256SUMS` and the `provenance.sigstore.json` attestation bundle, must be completely uploaded and have the same SHA-256 as the freshly built file, using GitHub's reported asset digest or, when absent, a downloaded copy; otherwise the job fails. The NuGet job (`release-tool nuget`) asks nuget.org whether the version exists. If not, it pushes without `--skip-duplicate`, so a concurrent push also fails. If so, it downloads the registry package and compares every entry's name and SHA-256 with the built package, ignoring only nuget.org's `.signature.p7s` repository countersignature (the registry's file bytes always differ from the local file because of it); any other difference fails. A failure means different bytes already exist under that version: a maintainer must delete the specific GitHub asset deliberately, or release a new version for NuGet. `dotnet run --project tools/PostgreSqlMcp.Verify -c Release -- release-regressions` (also run by `integration`) checks these comparisons and the zip/tar.gz archive builders offline. -- **Reproducible and non-reproducible assets.** The five archives (fixed timestamps, owners, modes and entry order; the Linux/macOS tar.gz use GNU tar format because .NET's PAX writer names extended headers after the process ID) and the six npm tarballs (main and platform packages) built by `tools/PostgreSqlMcp.Build` are byte-identical across rebuilds of the same source. The `.nupkg` (`dotnet pack` stamps wall-clock times on zip entries; its entry contents are identical) and the CycloneDX SBOM (random `serialNumber`, generation `timestamp`) are not, and neither is `SHA256SUMS`, which lists them, nor the provenance bundle, which is signed once per workflow run. Consequently, after a partial publication, re-run the failed jobs of the same workflow run (same artifacts, same digests) instead of dispatching a new run for the same tag, which would fail closed on those assets. +- **Reproducible and non-reproducible assets.** Built from the same source, the five archives (fixed timestamps, owners, modes and entry order; the Linux/macOS tar.gz use GNU tar format because .NET's PAX writer names extended headers after the process ID), the six npm tarballs (main and platform packages), the `.nupkg` and the CycloneDX SBOM are byte-identical across rebuilds, and so is `SHA256SUMS`, which lists them. `tools/PostgreSqlMcp.Build` packs the `.nupkg` with NuGet's supported `DeterministicTimestamp` property fixed to 1980-01-01 (the earliest ZIP time; an inherited `SOURCE_DATE_EPOCH` does not override it), so no entry carries the wall clock; the entry order and the `nuget.psmdcp` name are already stable. `release-tool sbom` replaces dotnet-CycloneDX's random `serialNumber` (the generator can only omit it) and wall-clock `timestamp` (no option) with the commit time of the checked-out release commit and a version 8 UUID derived from the SHA-256 of the BOM without its serial number. The provenance bundle is not reproducible: it is signed once per workflow run. Consequently, after a partial publication, re-run the failed jobs of the same workflow run (same artifacts, same digests) instead of dispatching a new run for the same tag: a new run rebuilds identical packages, but its provenance bundle differs from the published `provenance.sigstore.json` and fails closed. `release-regressions` checks the SBOM normalization and a double `dotnet pack` offline, and `packages --artifacts` requires the fixed timestamp on every built `.nupkg` entry. - **Dependency audit** checks direct and transitive NuGet packages against known advisories. This is not proof that all vulnerabilities are absent. - **CodeQL** scans C# and GitHub Actions with `security-extended`. C# uses real builds, including generated sources. Trusted main/scheduled runs publish code-scanning results; PRs scan both the base and candidate with read-only tokens, retain raw SARIF and run the trusted-base severity-delta gate. Extraction errors and missing reports fail closed. Review workflow changes as well as findings. - **Scorecard** publishes repository-practice findings on main and weekly. Its score is not a vulnerability-free claim or profile achievement. diff --git a/tools/PostgreSqlMcp.Build/Program.cs b/tools/PostgreSqlMcp.Build/Program.cs index 14647f9..47bbaf2 100644 --- a/tools/PostgreSqlMcp.Build/Program.cs +++ b/tools/PostgreSqlMcp.Build/Program.cs @@ -15,6 +15,9 @@ internal static class PackageBuilder private static readonly string[] Notices = ["LICENSE", "THIRD-PARTY-NOTICES.md"]; private static readonly string[] ServerProjects = ["PostgreSqlMcp", "PostgreSqlMcp.Core", "PostgreSqlMcp.Tools"]; private const string InstallerName = "PostgreSqlMcp.NpmInstall"; + // NuGet otherwise stamps every .nupkg entry with the wall clock. 1980-01-01 is the earliest ZIP time, matching the release archives; + // the explicit property also wins over a SOURCE_DATE_EPOCH inherited from the environment. + private const string PackageTimestamp = "1980-01-01T00:00:00Z"; internal static async Task RunAsync(string[] args) { @@ -83,7 +86,7 @@ await File.WriteAllTextAsync(Path.Combine(package, "README.md"), CreateTarball(main, mainTarball); var packed = Path.Combine(temporary, "nuget"); - await DotnetAsync(stage, "pack", server, "-c", "Release", "-o", packed); + await DotnetAsync(stage, "pack", server, "-c", "Release", "-o", packed, $"-p:DeterministicTimestamp={PackageTimestamp}"); var nupkg = Path.Combine(packed, nugetName); CheckNuget(nupkg); Directory.CreateDirectory(output); diff --git a/tools/PostgreSqlMcp.Release/Program.cs b/tools/PostgreSqlMcp.Release/Program.cs index b929e2e..7a6f734 100644 --- a/tools/PostgreSqlMcp.Release/Program.cs +++ b/tools/PostgreSqlMcp.Release/Program.cs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: MIT using System.Collections; +using System.Globalization; using System.Security.Cryptography; using System.Text.Json; using System.Text.RegularExpressions; @@ -58,11 +59,17 @@ public static async Task RunAsync(string[] args) case "sbom": await RunAsync("dotnet", ["tool", "restore"], root); await RunAsync("dotnet", ["dotnet-CycloneDX", "src/PostgreSqlMcp/PostgreSqlMcp.csproj", "--exclude-dev", "--disable-package-restore", "--output-format", "Json", "--output", "artifacts/sbom", "--filename", "bom.json", "--set-name", "codegiveness.postgresql-sharp-mcp", "--set-version", metadata.Version], root); - using (var document = JsonDocument.Parse(await File.ReadAllTextAsync(Path.Combine(root, "artifacts/sbom/bom.json")))) + string bom = Path.Combine(root, "artifacts/sbom/bom.json"); + using (var document = JsonDocument.Parse(await File.ReadAllTextAsync(bom))) { if (document.RootElement.GetProperty("bomFormat").GetString() != "CycloneDX" || document.RootElement.GetProperty("components").GetArrayLength() == 0) throw new InvalidOperationException("Generated SBOM lacks application dependencies."); } + // The checked-out commit's time (not the wall clock) keeps the SBOM byte-identical across rebuilds of the same source. + var commitTime = await CaptureAsync("git", ["log", "-1", "--format=%ct", "HEAD"], root); + if (commitTime.ExitCode != 0 || !long.TryParse(commitTime.Output.Trim(), NumberStyles.None, CultureInfo.InvariantCulture, out long commitSeconds)) + throw new InvalidOperationException("Cannot read the checked-out commit time for the SBOM timestamp."); + await File.WriteAllTextAsync(bom, SbomNormalizer.Normalize(await File.ReadAllTextAsync(bom), DateTimeOffset.FromUnixTimeSeconds(commitSeconds))); await ChecksumsAsync(root); break; case "github": diff --git a/tools/PostgreSqlMcp.Release/SbomNormalizer.cs b/tools/PostgreSqlMcp.Release/SbomNormalizer.cs new file mode 100644 index 0000000..6d04567 --- /dev/null +++ b/tools/PostgreSqlMcp.Release/SbomNormalizer.cs @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: 2026 codegiveness +// SPDX-License-Identifier: MIT + +using System.Globalization; +using System.Security.Cryptography; +using System.Text; +using System.Text.Json; +using System.Text.Json.Nodes; + +/// +/// Makes a generated CycloneDX JSON BOM a pure function of its content and the build time. dotnet-CycloneDX stamps a random +/// serialNumber and the wall-clock metadata.timestamp, and offers no option to fix either. +/// +internal static class SbomNormalizer +{ + private static readonly JsonSerializerOptions Indented = new() { WriteIndented = true }; + + /// + /// Sets metadata.timestamp and replaces serialNumber with a UUID (version 8, RFC 9562) taken from the SHA-256 of the + /// BOM without its serial number, so changed content or a different build time yields a different serial. + /// + public static string Normalize(string json, DateTimeOffset timestamp) + { + JsonObject bom = JsonNode.Parse(json) as JsonObject ?? throw new InvalidOperationException("SBOM is not a JSON object."); + JsonObject metadata = bom["metadata"] as JsonObject ?? throw new InvalidOperationException("SBOM has no metadata object."); + int serial = bom.IndexOf("serialNumber"); + int stamp = metadata.IndexOf("timestamp"); + if (serial < 0 || stamp < 0) throw new InvalidOperationException("SBOM lacks the serialNumber or metadata.timestamp to normalize."); + + bom.RemoveAt(serial); + metadata.SetAt(stamp, timestamp.UtcDateTime.ToString("yyyy-MM-dd'T'HH:mm:ss'Z'", CultureInfo.InvariantCulture)); + byte[] hash = SHA256.HashData(Encoding.UTF8.GetBytes(bom.ToJsonString())); + hash[6] = (byte)((hash[6] & 0x0F) | 0x80); + hash[8] = (byte)((hash[8] & 0x3F) | 0x80); + string hex = Convert.ToHexStringLower(hash.AsSpan(0, 16)); + bom.Insert(serial, "serialNumber", $"urn:uuid:{hex[..8]}-{hex[8..12]}-{hex[12..16]}-{hex[16..20]}-{hex[20..]}"); + return bom.ToJsonString(Indented) + "\n"; + } +} diff --git a/tools/PostgreSqlMcp.Verify/Packages.cs b/tools/PostgreSqlMcp.Verify/Packages.cs index 1282c1d..c5b14db 100644 --- a/tools/PostgreSqlMcp.Verify/Packages.cs +++ b/tools/PostgreSqlMcp.Verify/Packages.cs @@ -409,6 +409,7 @@ private static async Task VerifyArchivesAsync(string root, string tarball, strin Check.That(legal.IsSubsetOf(archive.Entries.Select(entry => entry.FullName)), "NuGet artifact lost legal notices."); foreach (ZipArchiveEntry entry in archive.Entries) { + Check.That(entry.LastWriteTime.DateTime == ReleaseArchive.ZipTimestamp.DateTime, $"NuGet entry {entry.FullName} carries a build-time stamp, so the package is not reproducible."); using Stream stream = entry.Open(); await VerifyNoWorkspacePathAsync(stream, workspacePaths); } diff --git a/tools/PostgreSqlMcp.Verify/PostgreSqlMcp.Verify.csproj b/tools/PostgreSqlMcp.Verify/PostgreSqlMcp.Verify.csproj index 5cbbd1a..62734da 100644 --- a/tools/PostgreSqlMcp.Verify/PostgreSqlMcp.Verify.csproj +++ b/tools/PostgreSqlMcp.Verify/PostgreSqlMcp.Verify.csproj @@ -11,6 +11,7 @@ + diff --git a/tools/PostgreSqlMcp.Verify/ReleaseRegressions.cs b/tools/PostgreSqlMcp.Verify/ReleaseRegressions.cs index 2190586..408f6ef 100644 --- a/tools/PostgreSqlMcp.Verify/ReleaseRegressions.cs +++ b/tools/PostgreSqlMcp.Verify/ReleaseRegressions.cs @@ -5,6 +5,8 @@ using System.IO.Compression; using System.Security.Cryptography; using System.Text; +using System.Text.Json; +using System.Text.RegularExpressions; namespace PostgreSqlMcp.Verify; @@ -16,6 +18,8 @@ public static void Run() AssetPlanning().GetAwaiter().GetResult(); ArchiveReproducibility(); NuGetPayloadComparison(); + SbomReproducibility(); + PackReproducibility().GetAwaiter().GetResult(); } private static LocalAsset Built(string name, string content) => @@ -141,4 +145,61 @@ string Package(string name, params (string Entry, string Content)[] entries) Check.That(NuGetRegistry.ComparePayload(local, Package("extra.nupkg", ("tool.nuspec", "spec"), ("tools/net10.0/any/app.dll", "binary"), ("tools/extra.dll", "x"))).Count == 1, "An entry only in the registry package must be reported."); } + + // dotnet-CycloneDX stamps a random serial and the wall clock; the release tool normalizes both from content and commit time. + private static void SbomReproducibility() + { + static string Generated(string serial, string timestamp, string component) => + $$$"""{"bomFormat":"CycloneDX","specVersion":"1.7","serialNumber":"urn:uuid:{{{serial}}}","version":1,"metadata":{"timestamp":"{{{timestamp}}}","component":{"name":"app"}},"components":[{"name":"{{{component}}}"},{"name":"a"}]}"""; + var buildTime = new DateTimeOffset(2026, 3, 4, 5, 6, 7, TimeSpan.FromHours(2)); + + string first = SbomNormalizer.Normalize(Generated(Guid.NewGuid().ToString(), "2026-10-11T02:38:39Z", "b"), buildTime); + string second = SbomNormalizer.Normalize(Generated(Guid.NewGuid().ToString(), "2031-01-01T00:00:00Z", "b"), buildTime); + Check.That(first == second, "SBOMs generated from the same content with different random serials and wall-clock times must normalize to identical bytes."); + + using JsonDocument document = JsonDocument.Parse(first); + JsonElement root = document.RootElement; + Check.That(root.EnumerateObject().Select(property => property.Name).SequenceEqual(["bomFormat", "specVersion", "serialNumber", "version", "metadata", "components"]) + && root.GetProperty("components").EnumerateArray().Select(component => component.GetProperty("name").GetString()).SequenceEqual(["b", "a"]), + "SBOM normalization must keep the generated property and component order."); + Check.That(root.GetProperty("metadata").GetProperty("timestamp").GetString() == "2026-03-04T03:06:07Z", "The SBOM timestamp must be the given build time in UTC."); + string serial = root.GetProperty("serialNumber").GetString()!; + Check.That(Regex.IsMatch(serial, "^urn:uuid:[0-9a-f]{8}-[0-9a-f]{4}-8[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$", RegexOptions.CultureInvariant), + "The SBOM serial number must be an RFC 4122 URN with a version 8 UUID."); + + string Serial(string json) => JsonDocument.Parse(json).RootElement.GetProperty("serialNumber").GetString()!; + Check.That(Serial(SbomNormalizer.Normalize(Generated("x", "y", "other"), buildTime)) != serial, "Changed SBOM content must change the serial number."); + Check.That(Serial(SbomNormalizer.Normalize(Generated("x", "y", "b"), buildTime.AddSeconds(1))) != serial, "A different build time must change the serial number."); + try { SbomNormalizer.Normalize("""{"bomFormat":"CycloneDX","metadata":{"timestamp":"t"}}""", buildTime); } + catch (InvalidOperationException) { return; } + throw new VerificationException("An SBOM without a serialNumber must be rejected, not silently accepted."); + } + + // Packs a dependency-free probe twice, more than the 2-second ZIP time resolution apart, with the property the package builder passes. + private static async Task PackReproducibility() + { + using TemporaryDirectory temporary = new(); + string project = Directory.CreateDirectory(Path.Combine(temporary.Path, "probe")).FullName; + File.WriteAllText(Path.Combine(project, "Probe.csproj"), + "net10.0Probe1.0.0"); + File.WriteAllText(Path.Combine(project, "Probe.cs"), "internal sealed class Probe { }"); + string dotnet = Processes.FindExecutable("dotnet"); + + async Task Pack(string output) + { + foreach (string folder in new[] { "bin", "obj" }) + if (Directory.Exists(Path.Combine(project, folder))) Directory.Delete(Path.Combine(project, folder), recursive: true); + await Processes.RunAsync(new(dotnet, "pack", "Probe.csproj", "-c", "Release", "-o", output, "-p:DeterministicTimestamp=" + ReleaseArchive.ZipTimestamp.ToString("yyyy-MM-dd'T'HH:mm:ss'Z'")), + timeout: 300, directory: project); + return Path.Combine(output, "Probe.1.0.0.nupkg"); + } + + string first = await Pack(Path.Combine(temporary.Path, "first")); + await Task.Delay(TimeSpan.FromSeconds(2.5)); + string second = await Pack(Path.Combine(temporary.Path, "second")); + Check.That(File.ReadAllBytes(first).AsSpan().SequenceEqual(File.ReadAllBytes(second)), "dotnet pack must produce identical .nupkg bytes with the fixed timestamp."); + using ZipArchive package = ZipFile.OpenRead(first); + Check.That(package.Entries.Count > 0 && package.Entries.All(entry => entry.LastWriteTime.DateTime == ReleaseArchive.ZipTimestamp.DateTime), + "Every .nupkg entry must carry the fixed timestamp."); + } } From 2b228e21b6188c124358780f7b8d48a4a6a8648c Mon Sep 17 00:00:00 2001 From: AG <288536783+codegiveness@users.noreply.github.com> Date: Sun, 11 Oct 2026 09:50:29 +0700 Subject: [PATCH 06/12] test: measure integration coverage of the server with coverlet and gate CI at 80% lines Add a verifier coverage command that instruments the shipped assemblies with the pinned coverlet console tool, runs the integration scenarios against the disposable fixture, and writes a Cobertura report. CI runs it in the verify job with an 80% total-line gate and uploads the report. --- .config/dotnet-tools.json | 4 + .github/workflows/ci.yml | 9 ++ CHANGELOG.md | 1 + CONTRIBUTING.md | 16 +++ tools/PostgreSqlMcp.Verify/Coverage.cs | 124 ++++++++++++++++++++++ tools/PostgreSqlMcp.Verify/Integration.cs | 5 +- tools/PostgreSqlMcp.Verify/Program.cs | 6 +- 7 files changed, 162 insertions(+), 3 deletions(-) create mode 100644 tools/PostgreSqlMcp.Verify/Coverage.cs diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json index 9059731..2aa31b1 100644 --- a/.config/dotnet-tools.json +++ b/.config/dotnet-tools.json @@ -9,6 +9,10 @@ "sharpfuzz.commandline": { "version": "2.3.0", "commands": ["sharpfuzz"] + }, + "coverlet.console": { + "version": "10.1.0", + "commands": ["coverlet"] } } } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 404a156..05d4935 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,6 +30,15 @@ jobs: run: dotnet format style postgresql-sharp-mcp.slnx --verify-no-changes --no-restore --severity warn - name: Real PostgreSQL MCP integration run: dotnet run --project tools/PostgreSqlMcp.Verify -c Release -- integration + # Re-runs the same scenarios with FLOSS coverlet instrumenting the real server processes; fails below the line-coverage gate. + - name: Measure statement coverage of the integration scenarios + run: dotnet run --project tools/PostgreSqlMcp.Verify -c Release -- coverage --threshold 80 + - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 + if: ${{ !cancelled() }} + with: + name: coverage + path: artifacts/coverage/coverage.cobertura.xml + if-no-files-found: warn - name: Build npm and NuGet packages with .NET run: dotnet run --project tools/PostgreSqlMcp.Build -c Release -- package --output artifacts/packages - name: Install and exercise both distributions diff --git a/CHANGELOG.md b/CHANGELOG.md index e85f8e8..813447e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ Documentation, policy and tooling only; no runtime behavior change. - SECURITY: document how vulnerability reports are handled. - Propose Silver and Gold Best Practices answers in `.bestpractices.json`, and record the live Scorecard figures, the reference project's figures, the criteria that remain unmet and an owner checklist in [docs/security-posture.md](docs/security-posture.md). Nothing is saved on bestpractices.dev until the owner reviews it. - Make the `.nupkg` and the CycloneDX SBOM reproducible, so every release asset except the per-run provenance bundle is byte-identical across rebuilds of the same source, and `SHA256SUMS` with them. `tools/PostgreSqlMcp.Build` passes NuGet's supported `DeterministicTimestamp` property to `dotnet pack`, fixing every zip entry time at 1980-01-01 (previously the wall clock; the entry order, contents and `nuget.psmdcp` name were already stable, so published package layouts are unchanged). `release-tool sbom` rewrites dotnet-CycloneDX's random `serialNumber` and wall-clock `metadata.timestamp` (the generator can only omit the serial number and cannot fix the timestamp) to a version 8 UUID derived from the SHA-256 of the BOM and the checked-out commit's time; the BOM stays valid CycloneDX JSON with the same properties and component order. `release-regressions` now proves both offline (SBOM normalization and two `dotnet pack` runs of a probe project more than two seconds apart), and `packages` rejects a `.nupkg` entry that carries a build-time stamp. The new `.nupkg` entry timestamps and SBOM fields apply to the next release; already published versions are unchanged. +- Verifier and CI: measure statement coverage of the real server under the integration scenarios with the pinned FLOSS coverlet console tool. `tools/PostgreSqlMcp.Verify -- coverage [--output dir] [--threshold n]` instruments the three shipped assemblies, runs `integration --no-build` (new flag that skips the build step) against the disposable Docker fixture, prints per-assembly line, branch and method coverage with the files and methods the scenarios never reach, and writes a Cobertura report. The CI `verify` job runs it with a gate of 80% total line coverage and uploads the report as the `coverage` artifact. The first measurement was 89.81% lines, 84.14% branches and 98.33% methods in total, excluding only compiler-generated `[GeneratedRegex]` code. It is a test-infrastructure change: no shipped runtime behavior changes. ## 0.5.0 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e6bd76f..4f9a8ee 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -79,6 +79,22 @@ dotnet run --project tools/PostgreSqlMcp.Verify -c Release -- release-regression For a bug, reproduce the failure before changing code and exercise the same path after the fix. Major new functionality must have automated consumer-visible behavior coverage; extend the maintained verifier with relevant boundaries, transitions and errors rather than duplicating wiring assertions. Add regression coverage for plausible consumer-visible failures or uncertain boundaries; avoid source-text, wiring-only and mock-echo assertions. Documentation-only changes need link and command review rather than unrelated database runs. +### Measure test coverage + +```bash +dotnet run --project tools/PostgreSqlMcp.Verify -c Release -- coverage [--output artifacts/coverage] [--threshold 80] +``` + +The `coverage` command builds the solution, restores the pinned [coverlet](https://github.com/coverlet-coverage/coverlet) console tool (MIT, `.config/dotnet-tools.json`), instruments the shipped `PostgreSqlMcp`, `PostgreSqlMcp.Core` and `PostgreSqlMcp.Tools` assemblies in place and runs `integration --no-build` as the coverlet target. Every scenario launches the real server as a child `dotnet PostgreSqlMcp.dll` process, and each process writes its hits when it exits, so one report merges all of them. Coverlet restores the original assemblies afterwards, and the next build repairs them if a run is killed. The command needs the same Docker fixture as `integration`. It prints coverlet's per-assembly line, branch and method table, then the files with the most unexecuted lines and the methods no scenario ever ran, and writes `coverage.cobertura.xml` under `artifacts/coverage` (ignored by git). `--threshold` fails the command when the **total line coverage** across the three assemblies is below the given percentage (the report is still written); CI passes 80. The command also fails when an assembly has no executed line, which would mean it was not instrumented or no server process flushed its hits. + +What the figures mean: + +- Coverlet counts a *line* as a statement: one unit per IL sequence point with source. It also reports branch and method coverage. The figures describe the `integration` scenarios only. `packages`, `release-regressions` and the fuzz campaign exercise other code, and the verifier itself is not measured. +- The server exits normally when the verifier closes its stdin, on `--validate`, on a refused startup and on SIGTERM (a .NET host turns SIGTERM into a graceful exit; confirmed with an instrumented server), so those runs keep their hits. A server killed with SIGKILL, such as a verifier deadline expiring on a failing run, loses its hits, so no scenario that passes today depends on that path. +- The only exclusion is `--exclude-by-file **/obj/**`: the `[GeneratedRegex]` implementations that the compiler generates under `obj`. Nothing handwritten is excluded or annotated with `ExcludeFromCodeCoverage`. +- Coverage is measured on the default PostgreSQL major (17) in the CI `verify` job and uploaded as the `coverage` artifact. The other majors run the same scenarios without instrumentation. +- Use the report to find gaps. A new feature's scenario must assert consumer-visible behavior, not merely execute lines. Raise the gate only to a level measured on a clean run, never to a figure that a flaky scenario might miss. + ## Build and install package artifacts These commands build npm and NuGet artifacts without publishing: diff --git a/tools/PostgreSqlMcp.Verify/Coverage.cs b/tools/PostgreSqlMcp.Verify/Coverage.cs new file mode 100644 index 0000000..2373d58 --- /dev/null +++ b/tools/PostgreSqlMcp.Verify/Coverage.cs @@ -0,0 +1,124 @@ +// SPDX-FileCopyrightText: 2026 codegiveness +// SPDX-License-Identifier: MIT + +using System.Globalization; +using System.Xml.Linq; + +namespace PostgreSqlMcp.Verify; + +/// +/// Measures how much of the shipped server the integration scenarios execute, with the FLOSS coverlet console tool +/// pinned in .config/dotnet-tools.json. Coverlet instruments the built server assemblies in place, runs +/// integration --no-build as its target (every scenario launches the real server as a child process), and +/// restores the originals afterwards. Each server process appends its hits when it exits, so the report merges all of them. +/// +internal static class Coverage +{ + // Statement coverage here is coverlet's line coverage: one unit per IL sequence point with a source line, which is how it counts statements. + private static readonly string[] Assemblies = ["PostgreSqlMcp", "PostgreSqlMcp.Core", "PostgreSqlMcp.Tools"]; + private const string CoberturaFile = "coverage.cobertura.xml"; + + public static async Task RunAsync(string root, string[] options) + { + string output = Path.Combine(root, "artifacts", "coverage"); + string? threshold = null; + for (int i = 0; i < options.Length;) + { + string option = options[i++]; + Check.That(i < options.Length, "Missing coverage option value."); + string value = options[i++]; + switch (option) + { + case "--output": output = Path.GetFullPath(value); break; + case "--threshold": + Check.That(double.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out double percent) && percent is > 0 and <= 100, + "--threshold must be a line coverage percentage above 0 and at most 100."); + threshold = percent.ToString(CultureInfo.InvariantCulture); + break; + default: throw new VerificationException("Unknown coverage option."); + } + } + + string solution = Path.Combine(root, "postgresql-sharp-mcp.slnx"); + // The server output directory holds all three assemblies (project references are copied locally), so one directory is instrumented. + string server = Path.Combine(root, "src", "PostgreSqlMcp", "bin", "Release", "net10.0"); + await Run(new("dotnet", "build", solution, "-c", "Release"), root, TimeSpan.FromMinutes(5)); + await Run(new("dotnet", "tool", "restore"), root, TimeSpan.FromMinutes(5)); + if (Directory.Exists(output)) Directory.Delete(output, recursive: true); + Directory.CreateDirectory(output); + + string[] arguments = + [ + "tool", "run", "coverlet", server, + "--target", "dotnet", + "--targetargs", $"\"{typeof(Coverage).Assembly.Location}\" integration --no-build", + "--format", "cobertura", "--output", output + Path.DirectorySeparatorChar, + // Release builds map sources to deterministic /_/ paths; without this coverlet finds no files on disk and skips every module. + "--exclude-assemblies-without-sources", "None", + // The only exclusion: compiler/source-generator output under obj (the [GeneratedRegex] implementations), which nobody writes or reviews. + "--exclude-by-file", "**/obj/**", + "--include", .. Assemblies.Select(name => $"[{name}]*") + ]; + if (threshold is not null) arguments = [.. arguments, "--threshold", threshold, "--threshold-type", "line", "--threshold-stat", "total"]; + ProcessResult result = await Run(new("dotnet", arguments), root, TimeSpan.FromMinutes(45), expected: null); + // Coverlet writes the report before it evaluates --threshold, so a threshold failure still leaves the artifact to inspect. + string report = Path.Combine(output, CoberturaFile); + Check.That(File.Exists(report), "Coverage run produced no Cobertura report."); + Summarize(report); + Check.That(result.ExitCode == 0, $"Coverage run failed with status {result.ExitCode}: a scenario failed or line coverage is below the requested threshold."); + Console.WriteLine($"Coverage report: {output}"); + } + + private static async Task Run(Command command, string directory, TimeSpan timeout, int? expected = 0) + { + ProcessResult result; + try + { + result = await ProcessRunner.RunAsync(new ProcessSpec(command.File, command.Arguments) + { + Timeout = timeout, Output = OutputPolicy.Inherit, WorkingDirectory = directory + }); + } + catch (TimeoutException) { throw new VerificationException($"{command.File} {command.Arguments[0]} exceeded its deadline."); } + if (expected is not null) + Check.That(result.ExitCode == expected, $"{command.File} {command.Arguments[0]} exited with status {result.ExitCode}; expected {expected}."); + return result; + } + + /// Lists the files with the most unexecuted lines and their wholly unexecuted methods, so the next scenario targets real gaps. + private static void Summarize(string cobertura) + { + XDocument report = XDocument.Load(cobertura); + foreach (string name in Assemblies) + Check.That(report.Descendants("package").Where(package => (string?)package.Attribute("name") == name) + .Descendants("line").Any(line => (int)line.Attribute("hits")! > 0), + $"The coverage report has no executed lines for {name}: it was not instrumented or no server process flushed its hits."); + var files = new Dictionary Lines, HashSet Covered, SortedSet Dead)>(StringComparer.Ordinal); + foreach (XElement type in report.Descendants("class")) + { + string file = (string?)type.Attribute("filename") ?? "?"; + if (!files.TryGetValue(file, out var entry)) files[file] = entry = ([], [], []); + foreach (XElement method in type.Descendants("method")) + { + var lines = method.Elements("lines").Elements("line").ToArray(); + if (lines.Length > 0 && lines.All(line => (int?)line.Attribute("hits") == 0)) + entry.Dead.Add($"{(string?)type.Attribute("name")}.{(string?)method.Attribute("name")}"); + } + foreach (XElement line in type.Elements("lines").Elements("line")) + { + int number = (int)line.Attribute("number")!; + entry.Lines.Add(number); + if ((int)line.Attribute("hits")! > 0) entry.Covered.Add(number); + } + } + Console.WriteLine("Least-covered files (unexecuted lines of lines):"); + foreach (var (file, entry) in files.OrderByDescending(pair => pair.Value.Lines.Count - pair.Value.Covered.Count).Take(10)) + { + int missed = entry.Lines.Count - entry.Covered.Count; + if (missed == 0) break; + Console.WriteLine($" {file}: {missed} of {entry.Lines.Count}"); + foreach (string method in entry.Dead.Take(8)) Console.WriteLine($" never run: {method}"); + if (entry.Dead.Count > 8) Console.WriteLine($" ... {entry.Dead.Count - 8} more never-run methods"); + } + } +} diff --git a/tools/PostgreSqlMcp.Verify/Integration.cs b/tools/PostgreSqlMcp.Verify/Integration.cs index 5333f4d..65fb38d 100644 --- a/tools/PostgreSqlMcp.Verify/Integration.cs +++ b/tools/PostgreSqlMcp.Verify/Integration.cs @@ -8,9 +8,10 @@ namespace PostgreSqlMcp.Verify; internal static class Integration { - public static async Task RunAsync(string root) + public static async Task RunAsync(string root, bool build = true) { - await Processes.RunAsync(new("dotnet", "build", Path.Combine(root, "postgresql-sharp-mcp.slnx"), "-c", "Release"), timeout: 180); + if (build) + await Processes.RunAsync(new("dotnet", "build", Path.Combine(root, "postgresql-sharp-mcp.slnx"), "-c", "Release"), timeout: 180); await ResourceChecks.RunAsync(); SarifRegression.Run(); ReleaseRegressions.Run(); diff --git a/tools/PostgreSqlMcp.Verify/Program.cs b/tools/PostgreSqlMcp.Verify/Program.cs index 56c2888..36327d0 100644 --- a/tools/PostgreSqlMcp.Verify/Program.cs +++ b/tools/PostgreSqlMcp.Verify/Program.cs @@ -17,6 +17,10 @@ public static async Task Main(string[] args) string root = RepositoryRoot.Find() ?? throw new VerificationException("Run the verifier from the repository or a child directory."); if (args is ["integration"]) await Integration.RunAsync(root); + else if (args is ["integration", "--no-build"]) + await Integration.RunAsync(root, build: false); + else if (args is ["coverage", .. var coverageOptions]) + await Coverage.RunAsync(root, coverageOptions); else if (args is ["sarif", "--baseline", var baseline, "--candidate", var candidate]) SarifGate.Run(baseline, candidate, Console.Out); else if (args is ["sarif-regressions"]) @@ -49,7 +53,7 @@ public static async Task Main(string[] args) await Packages.RunAsync(root, artifacts, targetsFile, installationOnly); } else - throw new VerificationException("Usage: integration | packages [--artifacts directory] [--targets-file disposable-targets.json | --installation-only] | sarif --baseline directory --candidate directory | sarif-regressions | release-regressions"); + throw new VerificationException("Usage: integration [--no-build] | coverage [--output directory] [--threshold line-percent] | packages [--artifacts directory] [--targets-file disposable-targets.json | --installation-only] | sarif --baseline directory --candidate directory | sarif-regressions | release-regressions"); return 0; } catch (Exception ex) From c6afb65002182d507eeb3fba711357a2346441ed Mon Sep 17 00:00:00 2001 From: AG <288536783+codegiveness@users.noreply.github.com> Date: Sun, 11 Oct 2026 09:52:03 +0700 Subject: [PATCH 07/12] feat!: verify TLS certificates by default A connection string without SSL Mode now uses VerifyFull for non-loopback hosts and Disable for loopback and Unix sockets instead of Npgsql's Prefer. Explicit SSL Mode values are honored unchanged. TLS failures map to fixed tls_verification_failed and tls_unavailable errors. The verifier gains disposable TLS-container scenarios. --- src/PostgreSqlMcp.Core/DatabaseRegistry.cs | 9 +- src/PostgreSqlMcp.Core/SecurityWarnings.cs | 19 +-- src/PostgreSqlMcp.Core/ServerOptions.cs | 5 +- src/PostgreSqlMcp.Core/SqlExecutor.cs | 10 +- src/PostgreSqlMcp.Core/TlsPolicy.cs | 67 +++++++++ src/PostgreSqlMcp/Program.cs | 9 ++ tools/PostgreSqlMcp.Verify/Integration.cs | 9 +- tools/PostgreSqlMcp.Verify/PostgresFixture.cs | 5 +- tools/PostgreSqlMcp.Verify/TlsChecks.cs | 113 +++++++++++++++ tools/PostgreSqlMcp.Verify/TlsFixture.cs | 129 ++++++++++++++++++ 10 files changed, 350 insertions(+), 25 deletions(-) create mode 100644 src/PostgreSqlMcp.Core/TlsPolicy.cs create mode 100644 tools/PostgreSqlMcp.Verify/TlsChecks.cs create mode 100644 tools/PostgreSqlMcp.Verify/TlsFixture.cs diff --git a/src/PostgreSqlMcp.Core/DatabaseRegistry.cs b/src/PostgreSqlMcp.Core/DatabaseRegistry.cs index a73e436..d5cc81e 100644 --- a/src/PostgreSqlMcp.Core/DatabaseRegistry.cs +++ b/src/PostgreSqlMcp.Core/DatabaseRegistry.cs @@ -73,7 +73,7 @@ public string GetDiscoveryDatabase(string target) yield return (target, _profiles[target].ConnectionString); } - internal DatabaseSelection Resolve(string database, string? target) + internal DatabaseSelection Resolve(string database, string? target, out string profileName) { if (string.IsNullOrEmpty(database)) throw new ToolException("invalid_target", "A database name or configured profile alias is required."); @@ -82,11 +82,18 @@ internal DatabaseSelection Resolve(string database, string? target) { if (!_profiles.TryGetValue(target, out profile!)) throw new ToolException("invalid_target", "Specify an exact configured connection profile as target."); + profileName = target; } else if (_profiles.TryGetValue(database, out profile!)) + { + profileName = database; database = profile.Database; + } else + { + profileName = DefaultTarget; profile = _profiles[DefaultTarget]; + } if (!IsDatabaseName(database)) throw new ToolException("invalid_target", "Specify a database name of 1..63 UTF-8 bytes without NUL, or a configured profile alias."); RequireAllowed(database); diff --git a/src/PostgreSqlMcp.Core/SecurityWarnings.cs b/src/PostgreSqlMcp.Core/SecurityWarnings.cs index 3173aa1..f21a699 100644 --- a/src/PostgreSqlMcp.Core/SecurityWarnings.cs +++ b/src/PostgreSqlMcp.Core/SecurityWarnings.cs @@ -1,7 +1,6 @@ // SPDX-FileCopyrightText: 2026 codegiveness // SPDX-License-Identifier: MIT -using System.Net; using Npgsql; namespace PostgreSqlMcp.Core; @@ -37,7 +36,7 @@ WHERE r.rolbypassrls AND pg_catalog.pg_has_role(session_user, r.oid, 'MEMBER') ORDER BY severity, warning COLLATE "C" """; - /// Profiles that reach a non-local host without certificate verification (Npgsql's default SSL Mode is Prefer). + /// Profiles that reach a non-local host without certificate verification because the operator set an explicit SSL Mode other than VerifyCA/VerifyFull. public static IReadOnlyList Tls(ServerOptions options) { // One line per distinct server login (host, port, user, mode) naming its profiles: lock aliases of one connection @@ -48,7 +47,7 @@ public static IReadOnlyList Tls(ServerOptions options) var builder = new NpgsqlConnectionStringBuilder(connectionString); if (builder.SslMode is SslMode.VerifyCA or SslMode.VerifyFull) continue; string host = builder.Host ?? ""; - if (host.Split(',').All(IsLocal)) continue; + if (TlsPolicy.IsLocalHost(host)) continue; var key = (host, builder.Port, builder.Username, builder.SslMode); if (!groups.TryGetValue(key, out List? aliases)) groups[key] = aliases = []; aliases.Add(alias); @@ -89,18 +88,4 @@ public static async Task> RolesAsync(DatabaseRegistry regi } return warnings; } - - private static bool IsLocal(string entry) - { - string host = entry.Trim(); - if (host.Length == 0 || host[0] is '/' or '@') return true; // Unix-domain socket directory or abstract socket. - if (host.StartsWith('[')) - { - int close = host.IndexOf(']'); - host = close > 0 ? host[1..close] : host; - } - else if (host.Count(c => c == ':') == 1) host = host[..host.IndexOf(':')]; // host:port - return host.Equals("localhost", StringComparison.OrdinalIgnoreCase) - || (IPAddress.TryParse(host, out IPAddress? address) && IPAddress.IsLoopback(address)); - } } diff --git a/src/PostgreSqlMcp.Core/ServerOptions.cs b/src/PostgreSqlMcp.Core/ServerOptions.cs index ed39d6e..efa70ce 100644 --- a/src/PostgreSqlMcp.Core/ServerOptions.cs +++ b/src/PostgreSqlMcp.Core/ServerOptions.cs @@ -108,7 +108,7 @@ int Number(string env, int fallback, int min, int max, string? key = null) // Lock-derived aliases of one connection string share a single login profile; only targets JSON/file defines several. profileCount = json is null ? 1 : targets.Count; if (profileCount is < 1 or > 32) throw new ToolException("configuration", "Configure 1..32 connection profiles in targets JSON/file."); - foreach (var (alias, connectionString) in targets) + foreach (var (alias, connectionString) in targets.ToArray()) { if (string.IsNullOrWhiteSpace(alias) || alias.Length > 128 || alias.Any(char.IsControl)) throw new ToolException("configuration", "Target aliases must be nonblank, <=128 characters, without control characters."); @@ -117,6 +117,9 @@ int Number(string env, int fallback, int min, int max, string? key = null) throw new ToolException("configuration", "Each target requires an explicit Host and Database."); if (builder.NoResetOnClose || builder.Multiplexing) throw new ToolException("configuration", "No Reset On Close and Multiplexing are not supported."); + // Targets hold the effective TLS mode, so warnings and pools see what the connection will really do. + TlsPolicy.ApplyDefault(builder); + targets[alias] = builder.ConnectionString; } } catch (Exception ex) when (ex is JsonException or ArgumentException or InvalidOperationException or IOException or UnauthorizedAccessException) diff --git a/src/PostgreSqlMcp.Core/SqlExecutor.cs b/src/PostgreSqlMcp.Core/SqlExecutor.cs index e48aa23..5af1c00 100644 --- a/src/PostgreSqlMcp.Core/SqlExecutor.cs +++ b/src/PostgreSqlMcp.Core/SqlExecutor.cs @@ -20,7 +20,7 @@ public Task QueryAsync(string database, string sql, IReadOnlyDictiona public async Task WithSessionAsync(string database, Func> action, bool readOnly = true, CancellationToken ct = default, string? target = null) { - DatabaseRegistry.DatabaseSelection selection = registry.Resolve(database, target); + DatabaseRegistry.DatabaseSelection selection = registry.Resolve(database, target, out string profile); if (!readOnly && !options.Unrestricted) throw new ToolException("read_only", "This server is in restricted access mode and refuses writes; use read_only=true."); using var deadline = CancellationTokenSource.CreateLinkedTokenSource(ct); deadline.CancelAfter(TimeSpan.FromSeconds(options.QueryTimeout)); @@ -31,7 +31,7 @@ public async Task WithSessionAsync(string database, Func WithSessionAsync(string database, Func OpenAsync(DatabaseRegistry.SourceEntry source, string profile, string connectionString, CancellationToken token) + { + try { return await source.Source.OpenConnectionAsync(token).ConfigureAwait(false); } + catch (NpgsqlException ex) when (TlsPolicy.Explain(ex, profile, connectionString) is { } tls) { throw tls; } + } + public void Dispose() => _calls.Dispose(); } diff --git a/src/PostgreSqlMcp.Core/TlsPolicy.cs b/src/PostgreSqlMcp.Core/TlsPolicy.cs new file mode 100644 index 0000000..bf17126 --- /dev/null +++ b/src/PostgreSqlMcp.Core/TlsPolicy.cs @@ -0,0 +1,67 @@ +// SPDX-FileCopyrightText: 2026 codegiveness +// SPDX-License-Identifier: MIT + +using System.Net; +using System.Security.Authentication; +using Npgsql; + +namespace PostgreSqlMcp.Core; + +/// +/// Default TLS behavior of connection profiles. Npgsql's own default, SSL Mode=Prefer, uses TLS without verifying the +/// server and silently falls back to plaintext, so the server never lets it apply by omission: a profile that does not set +/// SSL Mode is verified (non-local hosts) or does not use TLS at all (local hosts). An explicit SSL Mode is the +/// operator's decision and is kept unchanged. +/// +internal static class TlsPolicy +{ + private const string SslModeKeyword = "SSL Mode"; + + /// Sets SSL Mode on unless the operator set it. + public static void ApplyDefault(NpgsqlConnectionStringBuilder builder) + { + // ContainsKey reports every valid keyword; Keys lists only those present in the string, under canonical names. + if (builder.Keys.Cast().Contains(SslModeKeyword, StringComparer.Ordinal)) return; + // Over loopback or a Unix socket an on-path attacker is not in play, so unverified TLS buys nothing and would still be + // "TLS without verification": no TLS is used instead. A Root Certificate means the operator expects verified TLS. + builder.SslMode = IsLocalHost(builder.Host) && string.IsNullOrEmpty(builder.RootCertificate) + ? SslMode.Disable + : SslMode.VerifyFull; + } + + /// True when every entry of a (possibly multi-host) Host value is loopback or a Unix-domain socket. + public static bool IsLocalHost(string? host) => (host ?? "").Split(',').All(IsLocalEntry); + + private static bool IsLocalEntry(string entry) + { + string host = entry.Trim(); + if (host.Length == 0 || host[0] is '/' or '@') return true; // Unix-domain socket directory or abstract socket. + if (host.StartsWith('[')) + { + int close = host.IndexOf(']'); + host = close > 0 ? host[1..close] : host; + } + else if (host.Count(c => c == ':') == 1) host = host[..host.IndexOf(':')]; // host:port + return host.Equals("localhost", StringComparison.OrdinalIgnoreCase) + || (IPAddress.TryParse(host, out IPAddress? address) && IPAddress.IsLoopback(address)); + } + + /// + /// Turns a TLS-specific connection failure into a fixed, actionable error, or null for any other failure. The text names only + /// the profile: provider messages and the connection string can carry credentials and never reach the caller. + /// + public static ToolException? Explain(NpgsqlException ex, string profile, string connectionString) + { + SslMode mode = new NpgsqlConnectionStringBuilder(connectionString).SslMode; + // With VerifyCA/VerifyFull, Npgsql rejects an untrusted or mismatched certificate during the TLS handshake. + if (mode is SslMode.VerifyCA or SslMode.VerifyFull && ex.InnerException is AuthenticationException) + return new("tls_verification_failed", + $"TLS certificate verification failed for profile {profile}: the server certificate is not signed by a trusted CA or does not match the host name. " + + "Set Root Certificate= in the profile's connection string, or, to opt out explicitly, SSL Mode=Require (encrypted but unverified)."); + // The server answered the TLS request with 'N': it offers no TLS, and Npgsql does not fall back to plaintext in these modes. + if (mode is SslMode.Require or SslMode.VerifyCA or SslMode.VerifyFull && ex.InnerException is null && ex.Message.StartsWith("SSL connection requested", StringComparison.Ordinal)) + return new("tls_unavailable", + $"The server for profile {profile} does not offer TLS, but SSL Mode={mode} requires it. Enable TLS on the server, or, to opt out explicitly, set SSL Mode=Disable (no encryption)."); + return null; + } +} diff --git a/src/PostgreSqlMcp/Program.cs b/src/PostgreSqlMcp/Program.cs index 7b9efea..f6fff3b 100644 --- a/src/PostgreSqlMcp/Program.cs +++ b/src/PostgreSqlMcp/Program.cs @@ -25,6 +25,15 @@ 1..128 characters without control characters. Wins over a targets file. POSTGRES_TARGETS_FILE Path to a targets JSON file with the same format. --targets-file PATH Same; overrides POSTGRES_TARGETS_FILE. + TLS (SSL Mode inside each connection string): + Omitted Non-loopback host: SSL Mode=VerifyFull (certificate chain and host name verified + before any credential is sent; add Root Certificate= for a private CA). + Loopback or Unix socket: SSL Mode=Disable (no TLS). + Explicit (any value) Used unchanged. A remote host without VerifyCA/VerifyFull is warned about on stderr. + To opt out of verification explicitly, set SSL Mode=Require (or Prefer/Disable). + Failures tls_verification_failed (untrusted CA or host name mismatch) and tls_unavailable + (server offers no TLS) name the profile and the fix; no plaintext fallback. + Options: --databases JSON POSTGRES_DATABASES. Database lock: JSON array of 1+ unique physical names (1..63 UTF-8 bytes, no NUL), with a connection string or targets. Startup then diff --git a/tools/PostgreSqlMcp.Verify/Integration.cs b/tools/PostgreSqlMcp.Verify/Integration.cs index 65fb38d..def32a4 100644 --- a/tools/PostgreSqlMcp.Verify/Integration.cs +++ b/tools/PostgreSqlMcp.Verify/Integration.cs @@ -56,6 +56,7 @@ public static async Task RunAsync(string root, bool build = true) await VerifyWritesAsync(command, environment, fixture.Port); await VerifyConfigurationAsync(command, environment, targets, connection); await VerifySecurityWarningsAsync(command, environment, fixture); + await TlsChecks.RunAsync(command, environment, "Username=mcp_reader;Password=reader-disposable;Database=postgres;Timeout=5"); await VerifyDiscoveryAsync(command, environment, fixture, connection); await VerifyPoolCapacityAsync(command, environment, fixture, connection); await VerifyCancellationAsync(command, environment, fixture); @@ -746,7 +747,7 @@ private static async Task VerifySecurityWarningsAsync(Command command, Dictionar // Name resolution of .invalid fails without network traffic; the warning comes from configuration alone. // The login name is the marker: no part of the connection string may appear in a warning. const string remote = "Host=tls-warning.invalid;Username=sensitive-tls-marker;Database=d;Timeout=2"; - foreach (var (mode, warns) in new[] { ("", true), (";SSL Mode=Require", true), (";SSL Mode=VerifyFull", false) }) + foreach (var (mode, warns) in new[] { ("", false), (";SSL Mode=Prefer", true), (";SSL Mode=Require", true), (";SSL Mode=VerifyFull", false) }) { var tls = new Dictionary(environment) { ["POSTGRES_TARGETS"] = JsonSerializer.Serialize(new { remote = remote + mode }) }; ProcessResult result = await Processes.RunAsync(command.With("--validate"), tls, expected: 1); @@ -754,17 +755,19 @@ private static async Task VerifySecurityWarningsAsync(Command command, Dictionar $"Unverified-TLS warning was {(warns ? "missing" : "emitted")} for '{mode}'."); Check.Confidential(result.Error, "sensitive-tls-marker"); } + // An omitted SSL Mode is verified, so grouping is exercised with the explicit opt-out. + const string unverified = remote + ";SSL Mode=Prefer"; // Profiles on the same host but different ports are different servers: both are named. A connection-string lock's // aliases share one connection, so they share one line instead of repeating it per locked name. var ports = new Dictionary(environment) { - ["POSTGRES_TARGETS"] = JsonSerializer.Serialize(new { one = remote, two = remote + ";Port=6543" }) + ["POSTGRES_TARGETS"] = JsonSerializer.Serialize(new { one = unverified, two = unverified + ";Port=6543" }) }; ProcessResult grouped = await Processes.RunAsync(command.With("--validate"), ports, expected: 1); Check.That(grouped.Error.Contains("[warning] profile one: SSL Mode=", StringComparison.Ordinal) && grouped.Error.Contains("[warning] profile two: SSL Mode=", StringComparison.Ordinal), "A remote profile on another port was not named in an unverified-TLS warning."); - var locked = new Dictionary(environment) { ["POSTGRES_CONNECTION_STRING"] = remote, ["POSTGRES_DATABASES"] = "[\"d1\",\"d2\"]" }; + var locked = new Dictionary(environment) { ["POSTGRES_CONNECTION_STRING"] = unverified, ["POSTGRES_DATABASES"] = "[\"d1\",\"d2\"]" }; locked.Remove("POSTGRES_TARGETS"); ProcessResult shared = await Processes.RunAsync(command.With("--validate"), locked, expected: 1); Check.That(shared.Error.Contains("[warning] profiles d1, d2: SSL Mode=", StringComparison.Ordinal), diff --git a/tools/PostgreSqlMcp.Verify/PostgresFixture.cs b/tools/PostgreSqlMcp.Verify/PostgresFixture.cs index 7a36246..3d85b96 100644 --- a/tools/PostgreSqlMcp.Verify/PostgresFixture.cs +++ b/tools/PostgreSqlMcp.Verify/PostgresFixture.cs @@ -43,12 +43,15 @@ public static int Major } } + /// The digest-pinned official image for . + public static string Image => $"postgres:{Major}-bookworm@{ImageDigests[Major]}"; + public async Task StartAsync() { // This verifier never attaches to an existing container or operator database. attempted = true; await Processes.RunAsync(new("docker", "run", "-d", "--name", name, "-e", "POSTGRES_PASSWORD=mcp-disposable-only", - "-p", "127.0.0.1::5432", $"postgres:{Major}-bookworm@{ImageDigests[Major]}", "-c", "shared_preload_libraries=pg_stat_statements"), timeout: 180); + "-p", "127.0.0.1::5432", Image, "-c", "shared_preload_libraries=pg_stat_statements"), timeout: 180); started = true; bool ready = false; for (int attempt = 0; attempt < 60; attempt++) diff --git a/tools/PostgreSqlMcp.Verify/TlsChecks.cs b/tools/PostgreSqlMcp.Verify/TlsChecks.cs new file mode 100644 index 0000000..e73c204 --- /dev/null +++ b/tools/PostgreSqlMcp.Verify/TlsChecks.cs @@ -0,0 +1,113 @@ +// SPDX-FileCopyrightText: 2026 codegiveness +// SPDX-License-Identifier: MIT + +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace PostgreSqlMcp.Verify; + +/// +/// Verified TLS by default, against a disposable TLS-enabled PostgreSQL whose certificate is signed by a throwaway CA and names +/// the container's non-loopback bridge address. Proves that an omitted SSL Mode verifies the server before any credential is +/// sent, that the operator's explicit choices are honored, and that loopback uses no unverified TLS. +/// +internal static class TlsChecks +{ + private const string Failed = "tls_verification_failed"; + + /// The fixture's disposable reader login (user, password, database), without host or port. + public static async Task RunAsync(Command command, Dictionary environment, string login) + { + await using var tls = new TlsFixture(); + await tls.StartAsync(); + string bridge = $"Host={tls.BridgeAddress};Port=5432;{login}"; + string loopback = $"Host=127.0.0.1;Port={tls.LoopbackPort};{login}"; + string root = $";Root Certificate={tls.CaFile}"; + var targets = new Dictionary + { + ["bridge_default"] = bridge, + ["bridge_ca"] = bridge + root, + ["bridge_require"] = bridge + ";SSL Mode=Require", + ["loopback_default"] = loopback, + ["loopback_ca"] = loopback + root, + ["loopback_require"] = loopback + ";SSL Mode=Require", + // Trusted chain, but the certificate does not name "localhost": only host name verification can reject it. + ["name_mismatch"] = $"Host=localhost;Port={tls.LoopbackPort};{login}{root}", + // A multi-host string is local only if every host is: all-loopback uses no TLS, one remote host makes it verified. + ["multi_local"] = $"Host=127.0.0.1,localhost;Port={tls.LoopbackPort};{login}", + ["multi_mixed"] = $"Host=127.0.0.1:{tls.LoopbackPort},{tls.BridgeAddress}:5432;{login}" + }; + var served = new Dictionary(environment) { ["POSTGRES_TARGETS"] = JsonSerializer.Serialize(targets) }; + await using (var client = await McpClient.StartAsync(command, served)) + { + foreach (string alias in new[] { "bridge_default", "name_mismatch" }) + { + JsonNode failure = await client.FailsAsync("execute_sql", new { database = alias, sql = "SELECT 1" }, Failed); + string message = failure["error"]!["message"].Text(); + Check.That(message.Contains($"profile {alias}", StringComparison.Ordinal) && message.Contains("Root Certificate", StringComparison.Ordinal) + && message.Contains("SSL Mode=Require", StringComparison.Ordinal), "The certificate failure did not name the profile and both remedies."); + Check.Confidential(message, "reader-disposable"); + } + await client.FailsAsync("execute_sql", new { database = "multi_mixed", sql = "SELECT 1" }); + // The login must never reach a server whose certificate was not verified: the server saw no user at all. + Check.That(!(await tls.LogAsync()).Contains("mcp_reader", StringComparison.Ordinal), + "A login was sent to the server before its certificate was verified."); + var expected = new Dictionary + { + ["bridge_ca"] = "tls", ["bridge_require"] = "tls", ["loopback_default"] = "plaintext", ["loopback_ca"] = "tls", ["loopback_require"] = "tls", + ["multi_local"] = "plaintext" + }; + foreach (var (alias, transport) in expected) + { + JsonNode page = await client.OkAsync("execute_sql", new + { + database = alias, + sql = "SELECT CASE WHEN ssl THEN 'tls' ELSE 'plaintext' END AS transport FROM pg_stat_ssl WHERE pid = pg_backend_pid()" + }); + Check.That(Check.Rows(page).Single()["transport"].Text() == transport, $"Profile {alias} did not use {transport}."); + } + await client.StopAsync(); + string startup = client.StandardError; + Check.That(startup.Contains("[warning] profile bridge_require: SSL Mode=Require", StringComparison.Ordinal), + "An explicit unverified SSL Mode on a non-loopback host was not warned about."); + Check.That(new[] { "bridge_default", "bridge_ca", "loopback", "name_mismatch" }.All(name => !startup.Contains($"profile {name}", StringComparison.Ordinal)), + "A verified or loopback profile was reported as unverified."); + Check.Confidential(startup, "reader-disposable"); + } + Console.WriteLine("PASS verified TLS by default: unset SSL Mode verifies non-loopback hosts before sending credentials, explicit modes are honored, loopback uses no TLS"); + + // --validate: the same failures and successes as operator-facing diagnostics, on stderr only. + ProcessResult rejected = await Processes.RunAsync(command.With("--validate"), Single(environment, "bridge_default", bridge), expected: 1); + Check.That(rejected.Output.Length == 0, "Certificate failure contaminated protocol stdout."); + Check.That(rejected.Error.Contains(Failed, StringComparison.Ordinal) && rejected.Error.Contains("profile bridge_default", StringComparison.Ordinal) + && rejected.Error.Contains("SSL Mode=Require", StringComparison.Ordinal), "--validate did not report the certificate failure with its remedies."); + Check.Confidential(rejected.Error, "reader-disposable"); + ProcessResult trusted = await Processes.RunAsync(command.With("--validate"), Single(environment, "bridge_ca", bridge + root)); + Check.That(trusted.Output.Length == 0 && !trusted.Error.Contains("[warning]", StringComparison.Ordinal), "A verified connection was warned about or wrote to stdout."); + ProcessResult optedOut = await Processes.RunAsync(command.With("--validate"), Single(environment, "bridge_require", bridge + ";SSL Mode=Require")); + Check.That(optedOut.Output.Length == 0 && optedOut.Error.Contains("[warning] profile bridge_require: SSL Mode=Require", StringComparison.Ordinal), + "An explicit SSL Mode=Require did not connect with the unverified-TLS warning."); + Check.Confidential(trusted.Error + optedOut.Error, "reader-disposable"); + // Default for the base connection string, not only for targets JSON. + var baseString = new Dictionary(environment) { ["POSTGRES_CONNECTION_STRING"] = bridge }; + baseString.Remove("POSTGRES_TARGETS"); + ProcessResult baseRejected = await Processes.RunAsync(command.With("--validate"), baseString, expected: 1); + Check.That(baseRejected.Output.Length == 0 && baseRejected.Error.Contains(Failed, StringComparison.Ordinal), + "POSTGRES_CONNECTION_STRING did not default to verified TLS."); + Console.WriteLine("PASS --validate reports certificate failures on stderr with remedies and honors explicit SSL Mode"); + + // A server without TLS: the default must not silently fall back to plaintext, and Disable is the explicit opt-out. + await tls.DisableServerTlsAsync(); + ProcessResult plaintextRefused = await Processes.RunAsync(command.With("--validate"), Single(environment, "bridge_default", bridge), expected: 1); + Check.That(plaintextRefused.Output.Length == 0 && plaintextRefused.Error.Contains("tls_unavailable", StringComparison.Ordinal) + && plaintextRefused.Error.Contains("SSL Mode=Disable", StringComparison.Ordinal), "A non-TLS server was not refused with the TLS-unavailable error."); + ProcessResult plaintext = await Processes.RunAsync(command.With("--validate"), Single(environment, "bridge_disable", bridge + ";SSL Mode=Disable")); + Check.That(plaintext.Output.Length == 0 && plaintext.Error.Contains("[warning] profile bridge_disable: SSL Mode=Disable", StringComparison.Ordinal), + "An explicit SSL Mode=Disable did not connect with the unverified-TLS warning."); + Check.Confidential(plaintextRefused.Error + plaintext.Error, "reader-disposable"); + Console.WriteLine("PASS non-TLS server: refused by default without plaintext fallback, explicit SSL Mode=Disable honored"); + } + + private static Dictionary Single(Dictionary environment, string alias, string connection) => + new(environment) { ["POSTGRES_TARGETS"] = JsonSerializer.Serialize(new Dictionary { [alias] = connection }) }; +} diff --git a/tools/PostgreSqlMcp.Verify/TlsFixture.cs b/tools/PostgreSqlMcp.Verify/TlsFixture.cs new file mode 100644 index 0000000..1e9ef6d --- /dev/null +++ b/tools/PostgreSqlMcp.Verify/TlsFixture.cs @@ -0,0 +1,129 @@ +// SPDX-FileCopyrightText: 2026 codegiveness +// SPDX-License-Identifier: MIT + +using System.Globalization; +using System.Net; +using System.Security.Cryptography; +using System.Security.Cryptography.X509Certificates; + +namespace PostgreSqlMcp.Verify; + +/// +/// Disposable TLS-enabled PostgreSQL container. A throwaway CA and server certificate are generated per run and copied in; +/// the server certificate names the container's bridge address (a non-loopback host the verifier can reach) and 127.0.0.1. +/// Nothing is persisted and no key leaves the temporary directory, which is deleted when the fixture is disposed. +/// +internal sealed class TlsFixture : IAsyncDisposable +{ + public const string Password = "reader-disposable"; + private readonly string name = "postgresql-sharp-mcp-verify-tls-" + Guid.NewGuid().ToString("N"); + private readonly TemporaryDirectory files = new(); + private bool attempted; + private bool started; + + /// The container's address on Docker's bridge network, the non-loopback host the verifier connects to. + public string BridgeAddress { get; private set; } = ""; + /// Port published on 127.0.0.1 only. + public int LoopbackPort { get; private set; } + public string CaFile => Path.Combine(files.Path, "ca.pem"); + + public async Task StartAsync() + { + attempted = true; + // The entry point waits for the certificate files: the bridge address that the certificate must name exists only after the + // container runs. Postgres demands a key it owns with owner-only mode, so ownership is fixed inside the container. + await Processes.RunAsync(new("docker", "run", "-d", "--name", name, "-e", "POSTGRES_PASSWORD=mcp-disposable-only", + "-p", "127.0.0.1::5432", "--entrypoint", "sh", PostgresFixture.Image, "-c", + "until [ -e /tmp/certificates-ready ]; do sleep 0.2; done; chown postgres:postgres /tls/server.key; chmod 600 /tls/server.key; " + + "exec docker-entrypoint.sh postgres -c log_connections=on"), + timeout: 180); + started = true; + BridgeAddress = (await Processes.RunAsync(new("docker", "inspect", "-f", "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}", name))).Output.Trim(); + Check.That(IPAddress.TryParse(BridgeAddress, out IPAddress? parsed) && !IPAddress.IsLoopback(parsed), + "The TLS fixture needs a container address reachable from the verifier (Docker bridge network on Linux)."); + IPAddress address = parsed!; + string directory = Path.Combine(files.Path, "tls"); + Directory.CreateDirectory(directory); + WriteCertificates(directory, address); + await Processes.RunAsync(new("docker", "cp", directory, name + ":/")); + // TLS is configured through ALTER SYSTEM, not the command line, so a scenario can later turn it off with a reload. + string initialization = Path.Combine(files.Path, "tls.sql"); + File.WriteAllText(initialization, "ALTER SYSTEM SET ssl = on;\nALTER SYSTEM SET ssl_cert_file = '/tls/server.crt';\nALTER SYSTEM SET ssl_key_file = '/tls/server.key';\n"); + await Processes.RunAsync(new("docker", "cp", initialization, name + ":/docker-entrypoint-initdb.d/tls.sql")); + await Processes.RunAsync(new("docker", "exec", name, "touch", "/tmp/certificates-ready")); + bool ready = false; + for (int attempt = 0; attempt < 60; attempt++) + { + var result = await Processes.RunAsync(new("docker", "exec", name, "pg_isready", "-h", "127.0.0.1", "-U", "postgres"), expected: null); + if (result.ExitCode == 0) { ready = true; break; } + await Task.Delay(TimeSpan.FromSeconds(1)); + } + Check.That(ready, "Disposable TLS PostgreSQL fixture did not become ready."); + await Processes.RunAsync(new("docker", "exec", "-i", name, "psql", "-X", "-v", "ON_ERROR_STOP=1", "-U", "postgres"), + input: $"CREATE ROLE mcp_reader LOGIN PASSWORD '{Password}';"); + string endpoint = (await Processes.RunAsync(new("docker", "port", name, "5432/tcp"))).Output.Trim(); + LoopbackPort = int.Parse(endpoint[(endpoint.LastIndexOf(':') + 1)..], CultureInfo.InvariantCulture); + File.Copy(Path.Combine(directory, "ca.pem"), CaFile); + } + + /// Everything the server logged so far (PostgreSQL writes its log to stderr). + public async Task LogAsync() + { + ProcessResult result = await Processes.RunAsync(new("docker", "logs", name)); + return result.Output + result.Error; + } + + /// Turns TLS off on the running server (reload only), so it answers every TLS request with "not supported". + public async Task DisableServerTlsAsync() + { + await Processes.RunAsync(new("docker", "exec", "-i", name, "psql", "-X", "-v", "ON_ERROR_STOP=1", "-U", "postgres"), + input: "ALTER SYSTEM SET ssl = off;\nSELECT pg_reload_conf();\n"); + for (int attempt = 0; attempt < 50; attempt++) + { + ProcessResult shown = await Processes.RunAsync(new("docker", "exec", name, "psql", "-X", "-U", "postgres", "-tAc", "SHOW ssl")); + if (shown.Output.Trim() == "off") return; + await Task.Delay(TimeSpan.FromMilliseconds(200)); + } + throw new VerificationException("The TLS fixture did not apply ssl = off."); + } + + private static void WriteCertificates(string directory, IPAddress bridge) + { + DateTimeOffset from = DateTimeOffset.UtcNow.AddMinutes(-5), until = from.AddHours(2); + using ECDsa caKey = ECDsa.Create(ECCurve.NamedCurves.nistP256); + var caRequest = new CertificateRequest("CN=postgresql-sharp-mcp verifier CA", caKey, HashAlgorithmName.SHA256); + caRequest.CertificateExtensions.Add(new X509BasicConstraintsExtension(true, false, 0, true)); + caRequest.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.KeyCertSign | X509KeyUsageFlags.CrlSign, true)); + using X509Certificate2 ca = caRequest.CreateSelfSigned(from, until); + + using ECDsa serverKey = ECDsa.Create(ECCurve.NamedCurves.nistP256); + var serverRequest = new CertificateRequest("CN=postgresql-sharp-mcp verifier server", serverKey, HashAlgorithmName.SHA256); + serverRequest.CertificateExtensions.Add(new X509BasicConstraintsExtension(false, false, 0, true)); + serverRequest.CertificateExtensions.Add(new X509KeyUsageExtension(X509KeyUsageFlags.DigitalSignature, true)); + serverRequest.CertificateExtensions.Add(new X509EnhancedKeyUsageExtension([new Oid("1.3.6.1.5.5.7.3.1")], false)); + var names = new SubjectAlternativeNameBuilder(); + names.AddIpAddress(bridge); + names.AddIpAddress(IPAddress.Loopback); + serverRequest.CertificateExtensions.Add(names.Build()); + byte[] serial = RandomNumberGenerator.GetBytes(8); + serial[0] &= 0x7F; + serial[0] |= 0x01; + using X509Certificate2 server = serverRequest.Create(ca, from, until, serial); + + File.WriteAllText(Path.Combine(directory, "ca.pem"), ca.ExportCertificatePem()); + File.WriteAllText(Path.Combine(directory, "server.crt"), server.ExportCertificatePem()); + string key = Path.Combine(directory, "server.key"); + File.WriteAllText(key, serverKey.ExportPkcs8PrivateKeyPem()); + if (!OperatingSystem.IsWindows()) File.SetUnixFileMode(key, UnixFileMode.UserRead | UnixFileMode.UserWrite); + } + + public async ValueTask DisposeAsync() + { + try + { + if (attempted) + await Processes.RunAsync(new("docker", "rm", "-f", name), expected: started ? 0 : null); + } + finally { files.Dispose(); } + } +} From f20a870c817323a67680e2c47a6c4c4f3d35bc10 Mon Sep 17 00:00:00 2001 From: AG <288536783+codegiveness@users.noreply.github.com> Date: Sun, 11 Oct 2026 09:52:04 +0700 Subject: [PATCH 08/12] docs: document verified TLS by default and its upgrade path --- CHANGELOG.md | 3 ++- CONTRIBUTING.md | 2 ++ README.md | 14 ++++++++++---- SECURITY.md | 4 +++- docs/architecture.md | 1 + docs/assurance-case.md | 18 +++++++++--------- docs/roadmap.md | 1 - 7 files changed, 27 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 813447e..8ded7de 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,9 @@ ## Unreleased -Documentation, policy and tooling only; no runtime behavior change. +Upgrade note: connections to non-loopback hosts now verify the server certificate unless the connection string sets `SSL Mode` explicitly (see the first entry). Everything else below is documentation, policy and tooling only. +- **Breaking:** verified TLS by default. A connection string (base `POSTGRES_CONNECTION_STRING`/`--connection-string` or any targets JSON/file profile) that does not set `SSL Mode` previously got Npgsql's `Prefer`, which uses TLS without verifying the server and silently falls back to plaintext. It now gets `SSL Mode=VerifyFull` when its host is not loopback (certificate chain and host name are verified before any credential is sent, with no plaintext or unverified fallback) and `SSL Mode=Disable` when every host is `localhost`, `127.0.0.0/8`, `::1` or a Unix-domain socket (no TLS is used, so no TLS without verification; a `Root Certificate` on such a profile selects `VerifyFull`). An explicit `SSL Mode`, including `Prefer`, `Require` and `Disable`, is the operator's opt-out and is honored unchanged; the existing startup/`--validate` `[warning]` still flags non-loopback profiles without `VerifyCA`/`VerifyFull`. New fixed errors, on stderr for `--validate` and as structured tool errors, name only the profile and the fix: `tls_verification_failed` (untrusted CA or host name not in the certificate: add `Root Certificate=`, or opt out with `SSL Mode=Require`) and `tls_unavailable` (the server offers no TLS: enable it, or opt out with `SSL Mode=Disable`). **Upgrade guidance:** servers with a publicly trusted certificate need no change; a private CA needs `Root Certificate=`; a non-TLS remote server, including a host PostgreSQL reached as `host.docker.internal`, needs `SSL Mode=Disable`; a loopback or forwarded server that requires TLS (`hostssl` rules) needs an explicit `SSL Mode`. To restore the old behavior for a profile add `SSL Mode=Prefer` explicitly. Run `--validate` before reconnecting your client. `--help`, README, SECURITY and the assurance case are updated. Verifier: a disposable TLS container with a throwaway CA proves that an unset mode refuses an unverifiable non-loopback server before the login is sent (the server log shows no user), succeeds with `Root Certificate`, honors `Require` and `Disable` with the warning, rejects a host name missing from a trusted certificate, and uses plaintext on loopback. - Add [GOVERNANCE.md](GOVERNANCE.md) (decision-making, roles and what happens if the sole maintainer is unavailable; it states that `access_continuity` is not met), [docs/architecture.md](docs/architecture.md), [docs/roadmap.md](docs/roadmap.md) and [docs/assurance-case.md](docs/assurance-case.md) (requirements, threat model, trust boundaries, secure-design principles, common weaknesses and residual risks; the review record is pending the maintainer). - CONTRIBUTING: add coding standards, code-review standards and small-task guidance. Add an `.editorconfig` and a CI step (`dotnet format style --verify-no-changes --severity warn`) that enforces the C# style rules. Source files gain SPDX copyright and license headers, and unused `using` directives are removed. - SECURITY: document how vulnerability reports are handled. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4f9a8ee..f33f6fa 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -46,6 +46,8 @@ The fixture major version defaults to 17. Set `POSTGRES_FIXTURE_MAJOR` to 13, 14 The integration scenarios also install `pg_stat_statements` at extension versions 1.8 and 1.7 (available on 13–18) to verify that `get_top_queries` omits `toplevel` consistently and explains an outdated extension. +The TLS scenarios (`TlsChecks.cs`, `TlsFixture.cs`) start a second disposable container with TLS enabled and a throwaway CA and server certificate generated in C#, and connect to the container's Docker bridge address, a non-loopback host, to prove that an omitted `SSL Mode` verifies the server before any credential is sent, that explicit modes are honored and that loopback uses no TLS. They need a Docker Engine whose container addresses are reachable from the host (Linux); Docker Desktop on Windows and macOS does not route to them, so run this scenario on Linux or in CI. + For database-discovery/setup changes, exercise one inherited connection string selecting an unregistered physical database with no targets file, live creation/grant/revoke changes, optional protected multi-profile files, the database lock in both connection modes (database, alias and `target` rejection, filtered listing, strict-switch refusal and startup grant-check refusal/success), missing/denied selections without fallback, conflicting credential sources failing closed, and concurrent per-call isolation. Verify omitted-mode write commit/rollback with `read_only=false` separately from explicit restricted-mode refusal. Use disposable data; do not treat existing historical verification results as evidence that new behavior passed. Coverage includes explicit multi-target isolation, permissions/RLS, bounded results and pagination, metadata, health/workload tools, plans, hypothetical-index cleanup, explicit writes and rollback, sanitized errors, configuration failures and log confidentiality. Linux subprocess and MCP-client regressions exercise blocked stdin, inherited output pipes, failed-writer disposal and owned-child cleanup. Verification command deadlines cover stdin, exit and output drains; release capture deadlines cover exit and drains. MCP verification requests also bound semaphore wait, stdin writing and response wait with one cancellation-aware deadline. Asynchronous stdin close is bounded, and cleanup observes readers before disposal. An already-orphaned descendant is outside the exited parent's `Process.Kill` tree, so this is not an OS process-group/job-object containment guarantee. Report only the commands and scenarios actually exercised; passing one fixture does not prove every platform, PostgreSQL version or MCP client works. diff --git a/README.md b/README.md index fa302a6..2103d69 100644 --- a/README.md +++ b/README.md @@ -166,7 +166,7 @@ Enter one complete **.NET/Npgsql `key=value` connection string** at the hidden p Host=;Port=5432;Database=;Username=;Password=;SSL Mode=VerifyFull ``` -`Database` is an accessible starting database, such as your existing database or `postgres`; the same login can discover other accessible databases on that server. Use `VerifyFull` for remote TLS. For special characters, provider CA certificates or a local server without TLS, open [connection-string guidance](#connection-string-quoting-and-tls). +`Database` is an accessible starting database, such as your existing database or `postgres`; the same login can discover other accessible databases on that server. A remote server's certificate is verified by default (`SSL Mode=VerifyFull`, see [TLS](#connection-string-quoting-and-tls)). For special characters, provider CA certificates or a local server without TLS, open [connection-string guidance](#connection-string-quoting-and-tls). **Linux/macOS — paste into your current Bash or Zsh terminal.** No shell change or startup-file edit is needed. Then type or paste the connection string at the hidden prompt and press Enter: @@ -244,7 +244,9 @@ At the hidden prompt, quoting has **one layer: Npgsql**, not JSON or shell synta If your provider supplies only a PostgreSQL URL, map its host, port, database, username and password to the example's fields, decode URL-escaped values, and preserve required TLS/authentication options. The URL itself cannot be entered unchanged. -For a remote server needing a provider CA certificate, retain `SSL Mode=VerifyFull` and add `Root Certificate=`. Follow your provider's [TLS requirements](https://www.npgsql.org/doc/security.html); do not disable TLS to work around a remote certificate error. A remote profile without `VerifyCA`/`VerifyFull` (including the Npgsql default, `Prefer`) still works but prints a `[warning]` at startup and in `--validate`; `--validate` also warns when the login is a superuser or holds server-file, signal or checkpoint roles ([details](SECURITY.md#credentials-and-diagnostics)). For PostgreSQL on **this same computer** without TLS, use `Host=127.0.0.1` and replace `SSL Mode=VerifyFull` with `SSL Mode=Disable`. +**TLS is verified by default.** If a connection string does not set `SSL Mode`, the server uses `SSL Mode=VerifyFull` for every non-loopback host: the certificate chain **and** host name are verified before any credential is sent, and the connection never falls back to plaintext. A server whose certificate is signed by a public CA works unchanged; for a provider or private CA add `Root Certificate=` (see Npgsql's [TLS requirements](https://www.npgsql.org/doc/security.html)). The host name you connect to must appear in the certificate. For `localhost`, `127.0.0.0/8`, `::1` and Unix-domain sockets, an omitted `SSL Mode` means `SSL Mode=Disable`: nothing leaves the computer, and the server never uses TLS without verifying it (a `Root Certificate` on a loopback profile selects `VerifyFull` instead). A multi-host string counts as local only if every host is. Because only an omitted `SSL Mode` is rewritten, a PostgreSQL that requires TLS on loopback (`hostssl` rules only, or a forwarded remote port) needs an explicit `SSL Mode=Require` or `VerifyFull`. + +An explicit `SSL Mode` (any value) is your decision and is used unchanged. A remote profile with `Disable`, `Allow`, `Prefer` or `Require` still works, but prints a `[warning]` at startup and in `--validate` because the server is then not verified. Do not use these to work around a certificate error; fix the trust material instead. `--validate` also warns when the login is a superuser or holds server-file, signal or checkpoint roles ([details](SECURITY.md#credentials-and-diagnostics)). For PostgreSQL on **this same computer** without TLS, use `Host=127.0.0.1` and omit `SSL Mode`. ### Upgrading an existing installation @@ -252,6 +254,8 @@ Automatic discovery and the unrestricted default require **0.3.1 or newer**: the After publication, update an existing global .NET tool with `dotnet tool update --global codegiveness.postgresql-sharp-mcp --version 0.5.0`. For npx, update the version in your client's `args` to `@codegiveness/postgresql-sharp-mcp@0.5.0`; retain package-specific script approval. For archives, fully quit the client/server and replace the extracted files together. Recheck `--version` before reconnecting. +**Breaking behavior (verified TLS by default):** a profile that does not set `SSL Mode` used to get Npgsql's default `Prefer`, which encrypts without verifying the server and falls back to plaintext. It now gets `SSL Mode=VerifyFull` for a non-loopback host and `SSL Mode=Disable` for loopback/Unix-socket hosts. Remote servers with a publicly trusted certificate need no change. A remote server with a private CA fails with `tls_verification_failed`: add `Root Certificate=`. A remote server without TLS fails with `tls_unavailable`, including a local PostgreSQL reached as `host.docker.internal`. To restore the previous behavior for a profile, set `SSL Mode=Prefer` explicitly (or `Require`, or `Disable` for a server without TLS); the startup warning then names the profile. Check each profile with `--validate` before reconnecting your client. + **Breaking behavior:** a targets-file entry is now a host/login connection profile, not a database allowlist. The same credentials can select other physical databases on that PostgreSQL server. Omitted access mode means `unrestricted`; `list_databases` returns live databases rather than configured aliases. Preserve an existing explicit `POSTGRES_ACCESS_MODE=restricted` and review PostgreSQL grants before upgrading. **Rolling back the 0.4.0 lock startup refusal:** if an existing `POSTGRES_DATABASES`/`--databases` configuration is refused after upgrading, you do not have to give up the upgrade permanently. Read the refusal findings on stderr, then either (a) pin the previous release (**0.3.3**: `dotnet tool update --global codegiveness.postgresql-sharp-mcp --version 0.3.3`, or `@0.3.3` in the npx `args`), which enforces the list inside the server without the PostgreSQL grant check, and **remove `--require-database-lock` from `args`** because 0.3.3 does not recognize it and exits with "Unknown option" (a `POSTGRES_REQUIRE_DATABASE_LOCK` environment variable is ignored by 0.3.3, so it is harmless there but no longer enforces anything), or (b) temporarily remove `POSTGRES_DATABASES`/`--databases` and any `POSTGRES_REQUIRE_DATABASE_LOCK`/`--require-database-lock` setting so the server starts on 0.4.0. Both are stopgaps: 0.3.3 accepts a lock only with a connection string (not with targets JSON/file), and removing the lock leaves the login's full PostgreSQL privileges reachable, so keep that login least-privileged and restore the lock soon. Fix the findings with the [hardening guide](SECURITY.md#database-lock-hardening), restore the lock, and re-run `--validate` before reconnecting the client. @@ -333,7 +337,8 @@ Use your actual paths and keep validation stderr private. Reload the saved MCP c | .NET runtime not found | npm 0.3.2 and earlier require .NET; upgrade to 0.3.3 or newer after publication or use a local fixed artifact. The .NET tool still requires .NET 10 and `DOTNET_ROOT` for custom runtime locations. npm 0.3.3 and newer and standalone archives bundle the runtime. | | Invalid configuration / missing credentials | Repeat the hidden prompt, clear stale credential sources from the shell/client entry, and start a new client from that shell. Raw PostgreSQL URLs are not accepted. For files, check the path, JSON escaping and accidental `.txt` extension. | | Connection refused, timeout or authentication error | Run `--validate`; check host, port, database, login/password, network/VPN and PostgreSQL access rules. Installing this app does not grant access. | -| Certificate validation error | Use the provider's correct hostname and CA certificate. Do not disable remote TLS. | +| `tls_verification_failed` (certificate validation error) | The server certificate is not signed by a trusted CA or does not name the host you connect to. Use the provider's host name and add `Root Certificate=`. Do not switch to `Prefer`/`Require` to hide it; `SSL Mode=Require` is the explicit, warned opt-out when verification is impossible. `Trust Server Certificate=true` does not bypass `VerifyFull`. | +| `tls_unavailable` | The server does not offer TLS and the profile requires it (the default for non-loopback hosts). Enable TLS on the server, or set `SSL Mode=Disable` explicitly for a trusted network. | | No PostgreSQL tools in OMP | Check MCP configuration location/schema and executable path, then fully quit and launch OMP from the prepared shell. Reload alone cannot supply a missing inherited secret. | | Invalid target / database missing or denied | `target` must match a configured profile; `database` may be a real name. Check spelling, CONNECT and object privileges. Selection never falls back. | | Server waits silently without `--validate` | Normal: stdio mode waits for MCP messages. Let the client launch it; use `--validate` for a terminal check. | @@ -384,7 +389,7 @@ MCP client entry. It carries no credential: `-e POSTGRES_CONNECTION_STRING` with Omit the `POSTGRES_DATABASES`/`POSTGRES_REQUIRE_DATABASE_LOCK` lines (both `-e` pairs and `env` entries) if you do not use a [database lock](#database-lock); the lock needs the PostgreSQL grants described in the [hardening guide](SECURITY.md#database-lock-hardening). -- **`Host=127.0.0.1` or `localhost` inside the container is the container itself,** not your computer. For PostgreSQL on the Docker host use a host name the container can resolve, such as `host.docker.internal` (Docker Desktop; on Linux Engine add `"--add-host=host.docker.internal:host-gateway"` to `args` before the image name), or `"--network", "host"` on Linux. For a remote server use its real host name and keep `SSL Mode=VerifyFull`. +- **`Host=127.0.0.1` or `localhost` inside the container is the container itself,** not your computer. For PostgreSQL on the Docker host use a host name the container can resolve, such as `host.docker.internal` (Docker Desktop; on Linux Engine add `"--add-host=host.docker.internal:host-gateway"` to `args` before the image name), or `"--network", "host"` on Linux. `host.docker.internal` is not loopback, so TLS is verified by default: a host PostgreSQL without TLS needs `SSL Mode=Disable` in the connection string. For a remote server use its real host name and keep `SSL Mode=VerifyFull`. - The container environment is plaintext to Docker inspection and privileged operators; it is not a secret store. Restart the container after credential changes. - For a [protected targets file](#optional-protected-targets-file) instead, clear `POSTGRES_CONNECTION_STRING` and add a read-only bind mount plus `-e POSTGRES_TARGETS_FILE=/run/postgres.targets.json` (full command in [CONTRIBUTING](CONTRIBUTING.md#self-contained-executable-and-container)). The container runs as a **non-root user**, so a `chmod 600` file owned by your account is unreadable inside it and startup fails with the generic "Invalid targets configuration" error. Grant that user read access through a suitable group/UID or a secret mount; do not make the file world-readable. @@ -583,6 +588,7 @@ For one server, use inherited `POSTGRES_CONNECTION_STRING`; it creates the `prim | `POSTGRES_TARGETS` | JSON profile-name-to-bootstrap-connection-string object; each string needs an explicit `Host` and `Database`; aliases are nonblank, ≤128 characters, without control characters; wins over `POSTGRES_TARGETS_FILE` | | `POSTGRES_TARGETS_FILE` | Protected JSON profile file; `--targets-file` supported and overrides it | | `POSTGRES_CONNECTION_STRING` | Base Npgsql string; `Database` defaults to `postgres` if omitted **in this connection-string mode only** (targets profiles must include `Database`); wins over `--connection-string` | +| `SSL Mode` (inside each connection string) | Omitted: `VerifyFull` for non-loopback hosts, `Disable` for loopback and Unix sockets. Explicit values, including `Prefer` and `Require`, are used unchanged; `Root Certificate=` supplies the CA for `VerifyCA`/`VerifyFull` | | `POSTGRES_DATABASES` | Optional database lock: JSON array of 1+ unique physical database names (each 1..63 UTF-8 bytes, no NUL; no 32-name limit; with a connection string the names are `target` aliases of one login profile and do not count toward the profile limit), with a connection string or targets JSON/file; startup verifies PostgreSQL grants enforce it; `--databases` supported | | `POSTGRES_REQUIRE_DATABASE_LOCK` | `false`; `true` makes a missing `POSTGRES_DATABASES` a startup error; `--require-database-lock` supported | | `POSTGRES_ACCESS_MODE` | unrestricted when omitted; opt into restricted to refuse writes; `--access-mode` supported | diff --git a/SECURITY.md b/SECURITY.md index 7d250a2..ba8c494 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -136,7 +136,9 @@ These protections are not content redaction for successful results. Queries, pla ## Connection and resource settings -TLS and certificate trust are operator-controlled Npgsql connection-string settings. The server does not force TLS or verify certificates on the operator's behalf. For remote production connections, configure certificate validation appropriate to the deployment, such as `SSL Mode=VerifyFull`, and provision the necessary trust material. Npgsql's default `SSL Mode=Prefer`, like `Require`, encrypts without verifying the server, so an on-path attacker can impersonate it and capture the login; startup and `--validate` print a non-blocking `[warning]` for each profile that reaches a non-loopback host without `VerifyCA` or `VerifyFull` (Unix sockets and loopback addresses are exempt). Use network controls to restrict database reachability. +**TLS is verified by default.** A connection string (the base `POSTGRES_CONNECTION_STRING`/`--connection-string` or any targets JSON/file profile) that does not set `SSL Mode` is rewritten at startup, before any connection exists: to `SSL Mode=VerifyFull` when its host is not loopback, so the server certificate chain and host name are verified during the TLS handshake and no credential, not even the user name, is sent to an unverified server; to `SSL Mode=Disable` when every host is `localhost`, in `127.0.0.0/8`, `::1` or a Unix-domain socket, so no TLS without verification is ever used (a `Root Certificate` on such a profile selects `VerifyFull` instead). The operator supplies trust material (`Root Certificate=`, or the system store for public CAs). If verification fails, no connection is made and there is no fallback to plaintext or to unverified TLS: the tool or `--validate` reports the fixed errors `tls_verification_failed` (untrusted CA or host name not in the certificate) or `tls_unavailable` (the server offers no TLS), naming only the profile and the remedy. The message and the diagnostics never contain the connection string, credentials or provider text. + +An explicit `SSL Mode`, any value, is the operator's opt-out and is honored unchanged, so `Prefer`, `Allow`, `Require` or `Disable` can still be chosen deliberately. Those encrypt without verifying the server (or not at all), so an on-path attacker can impersonate it and capture the login; startup and `--validate` therefore print a non-blocking `[warning]` for each profile that reaches a non-loopback host without `VerifyCA` or `VerifyFull`. Because the default changed from Npgsql's `Prefer` (see the [upgrade note](README.md#upgrading-an-existing-installation)), existing remote profiles with a private CA need `Root Certificate`, and remote servers without TLS need an explicit `SSL Mode=Disable`. TLS protocol versions and cipher suites are negotiated by .NET and the PostgreSQL server. Use network controls to restrict database reachability. The server owns pool bounds, reset-on-close, transaction enlistment, multiplexing, application name, command timeouts, and logging safety settings. Configuration with `No Reset On Close=true` or multiplexing is rejected. Operation deadlines include queue and connection-pool wait; transactions also set statement and lock timeouts. Result row, byte, cell, and column limits bound responses. These are resource controls, not a guarantee against expensive queries, external side effects, or denial of service. Apply PostgreSQL and operating-system resource controls where needed. diff --git a/docs/architecture.md b/docs/architecture.md index c95b08e..5960355 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -43,6 +43,7 @@ Three projects keep one dependency direction, `Core <- Tools <- App`: | `QueryPage.cs` | Result records: columns, rows, truncation, `next_offset`. | | `DatabaseLockCheck.cs` | At startup and in `--validate`, proves in PostgreSQL that a locked login cannot reach other databases (superuser, server-file roles, `CREATEROLE`/`CREATEDB`, owned or connectable databases, `dblink`/`postgres_fdw`, server-file function grants). | | `SecurityWarnings.cs` | Non-blocking stderr warnings: non-loopback hosts without certificate verification, and privileged role capabilities. | +| `TlsPolicy.cs` | Applies the TLS default to every profile that omits `SSL Mode` (`VerifyFull` for non-loopback hosts, `Disable` for loopback and Unix sockets; explicit values are kept) and maps TLS handshake failures to fixed `tls_verification_failed`/`tls_unavailable` errors. | ### Tools diff --git a/docs/assurance-case.md b/docs/assurance-case.md index 9358ac3..a36eceb 100644 --- a/docs/assurance-case.md +++ b/docs/assurance-case.md @@ -16,7 +16,7 @@ The software is a local stdio MCP server that exposes PostgreSQL operations usin | R4 | **Credentials and data stay out of diagnostics.** Logs and errors contain no connection strings, passwords, SQL values or row data beyond what an authorized, successful result returns. stdout carries only MCP JSON-RPC. | | R5 | **Bounded resource use.** Result size, statement duration, concurrency and connection counts are bounded, so one call cannot exhaust the host through the server's own behavior. | | R6 | **Inputs are validated.** Every tool argument, option and configuration value is checked against an allowlist, length or range, and values reach PostgreSQL only as bound parameters or fixed SQL text. | -| R7 | **Transport security is delegated and visible.** TLS and authentication come from Npgsql and .NET under the operator's connection-string settings; the server warns on unverified remote connections. | +| R7 | **Transport security is verified by default.** An omitted `SSL Mode` becomes `VerifyFull` for non-loopback hosts and `Disable` for loopback; an explicit `SSL Mode` is the operator's opt-out and is warned about on remote hosts. TLS and authentication otherwise come from Npgsql and .NET. | | R8 | **Released artifacts are verifiable.** Each release has SHA-256 checksums, an SBOM and signed build provenance, built from locked dependencies by pinned automation. | | R9 | **Development is gated.** Changes reach `main` through pull requests with passing build, test, static analysis, dependency, secret and container checks. No secrets are stored in the repository. | @@ -26,7 +26,7 @@ The software is a local stdio MCP server that exposes PostgreSQL operations usin | N2 | **Protection against a malicious or compromised client.** There is no MCP authentication or per-tool authorization; whoever controls the client can use the login. | | N3 | **Protection from over-privileged roles.** A superuser or a member of `pg_execute_server_program`, `pg_read_server_files` and similar roles can act beyond SQL reads. `--validate` warns; it does not prevent. | | N4 | **Redaction of successful results.** Authorized results may contain sensitive data and are returned as-is within the limits. | -| N5 | **Verified TLS by default.** Npgsql's default `SSL Mode=Prefer` does not verify the server and can fall back to plaintext ([residual risks](#7-residual-risks-and-known-gaps)). | +| N5 | **Verified TLS by default.** A connection string without `SSL Mode` verifies the server certificate chain and host name before any credential is sent (non-loopback hosts) or uses no TLS (loopback and Unix sockets); it never uses Npgsql's `Prefer`, which does not verify the server and can fall back to plaintext. Only an explicit `SSL Mode` changes this ([residual risks](#7-residual-risks-and-known-gaps)). | | N6 | **Resilience against expensive queries or a total memory/CPU ceiling.** Limits bound the server's behavior, not what PostgreSQL does with an expensive statement. | | N7 | **Defense against prompt injection.** The server cannot tell whether a tool call or returned data was influenced by hostile text. Read-only defaults and the database lock limit the consequences. | @@ -42,7 +42,7 @@ The software is a local stdio MCP server that exposes PostgreSQL operations usin | T2 | SQL written to escape the read-only transaction or change session state | `SqlGuard` rejects multiple statements and transaction/session control; PostgreSQL enforces the read-only transaction | `set_config()` and privileged routines are not blocked; the guard is not an authorization parser | | T3 | Reaching other databases on the server than intended | R3 lock enforced in every path that opens a connection, plus startup proof in PostgreSQL | Startup snapshot: later grant changes are not re-verified until restart or `--validate` | | T4 | Credential disclosure through logs, errors, process list or files | R4; configuration errors use fixed text; Npgsql error detail and parameter logging forced off; SDK logging disabled | Command-line connection strings appear in process arguments; environment and files are plaintext readable by privileged local users | -| T5 | Network attacker impersonates or sniffs the PostgreSQL connection | Operator sets `SSL Mode=VerifyFull`; server warns on non-loopback hosts without verification | Default `Prefer` is unverified and may be plaintext; the server does not force TLS | +| T5 | Network attacker impersonates or sniffs the PostgreSQL connection | Omitted `SSL Mode` is `VerifyFull` for non-loopback hosts, so an unverifiable server receives no credential and there is no plaintext fallback; server warns on explicit unverified modes | An operator who sets `Prefer`, `Allow`, `Require` or `Disable` explicitly accepts impersonation or sniffing; a CA the operator trusts is trusted completely | | T6 | Resource exhaustion through large results, long statements, many concurrent calls or pool growth | R5 limits and deadlines | PostgreSQL still executes expensive statements until the statement timeout | | T7 | Malformed configuration or arguments cause misbehavior | R6 allowlists, bounds, fixed errors, rejection of conflicting sources | None known beyond the documented limits | | T8 | A compromised dependency, action, build tool or release path ships malicious code | Locked NuGet restore, Dependabot, dependency audit, full-SHA-pinned actions, checksum-pinned downloaded tools, container scan, attested releases, OIDC publishing without long-lived tokens | Trust in upstream packages and in the maintainer's account; single maintainer | @@ -75,7 +75,7 @@ The software is a local stdio MCP server that exposes PostgreSQL operations usin | R4 | `ToolReply.cs` returns SQLSTATE plus fixed summaries; PostgreSQL text only for class-42 errors positioned inside the caller's statement or `42501`, and not from routines or internal queries. Npgsql `IncludeErrorDetail=false`, `LogParameters=false`; MCP and Npgsql log categories off; only `Program.cs` writes to stderr. | `Verify`: log-confidentiality and sanitized-error scenarios | | R5 | Limits: SQL 131,072 characters; 256 parameters; rows 1,000 (max 5,000); result 64 KiB (max 1 MiB); cell 4,096 characters; 128 columns; offset 1,000,000; query timeout 30 s (max 600 s); 16 concurrent calls; pool size 8 and at most `floor(256 / pool size)` cached data sources; statement and lock timeouts set per transaction. | `Verify`: Boundaries, Pagination, Pools, PoolCapacity, Cancellation; `ResourceChecks.cs` | | R6 | Allowlists for `order_by`, `section`, `type`, `format`, access mode; ranges for every numeric option; JSON scalars only for parameters; identifiers bound as parameters; only extension schema names are quoted into SQL text. `SqlGuard` is additionally fuzzed. | `Verify`: Parameters, FsCheck properties (`FuzzChecks.cs`); `tools/PostgreSqlMcp.Fuzz` with checked-in corpus | -| R7 | The code sets no TLS options and implements no cryptography; `SecurityWarnings.cs` warns for non-loopback hosts unless `VerifyCA`/`VerifyFull`. | `Verify`: SecurityWarnings scenarios | +| R7 | `TlsPolicy.cs` sets `SSL Mode` on every profile that omits it (`ServerOptions.Parse`), and `SqlExecutor` turns TLS failures into fixed `tls_verification_failed`/`tls_unavailable` errors; the code implements no cryptography. `SecurityWarnings.cs` warns for explicit unverified modes on non-loopback hosts. | `Verify`: TlsChecks scenarios against a disposable TLS container and a throwaway CA; SecurityWarnings scenarios | | R8 | Release workflow builds locked dependencies into archives, npm and NuGet packages, a CycloneDX SBOM, `SHA256SUMS` and GitHub attestations; since 0.5.0 the attestation bundle is a release asset. | [Supply-chain evidence](security-posture.md#supply-chain-evidence); `release-regressions` verifier command | | R9 | CI, CodeQL, dependency audit, Gitleaks, Trivy and fuzzing run on every PR and push to `main`; the `main` ruleset requires them. | Workflow runs on `64c55c7`, 2026-10-11: [CI](https://github.com/codegiveness/postgresql-sharp-mcp/actions/runs/38102103515), [CodeQL](https://github.com/codegiveness/postgresql-sharp-mcp/actions/runs/38102103505), [dependency audit](https://github.com/codegiveness/postgresql-sharp-mcp/actions/runs/38102103499), [secret scanning](https://github.com/codegiveness/postgresql-sharp-mcp/actions/runs/38102103514), [container security](https://github.com/codegiveness/postgresql-sharp-mcp/actions/runs/38102103513), [fuzzing](https://github.com/codegiveness/postgresql-sharp-mcp/actions/runs/38102103518) all succeeded | @@ -87,8 +87,8 @@ The eight principles of [Saltzer and Schroeder](https://web.mit.edu/Saltzer/www/ | Principle | How it is applied | |---|---| -| Economy of mechanism | About 2,300 lines of C# in `src/`, 16 files, three projects, one transport (stdio), no listener, no HTTP client, no plugin mechanism. | -| Fail-safe defaults | SQL calls default to `read_only=true`; invalid or conflicting configuration stops startup; failed database selection never falls back to another database; unknown options are rejected. The one default that is not deny-by-default is **unrestricted access mode when the mode is omitted**, which is documented, and writes still require an explicit `read_only=false`. TLS verification is also not the default ([N5](#1-security-requirements)). | +| Economy of mechanism | About 2,400 lines of C# in `src/`, 17 files, three projects, one transport (stdio), no listener, no HTTP client, no plugin mechanism. | +| Fail-safe defaults | SQL calls default to `read_only=true`; invalid or conflicting configuration stops startup; failed database selection never falls back to another database; unknown options are rejected; TLS verification is the default for non-loopback hosts and a certificate failure never falls back to plaintext or unverified TLS ([N5](#1-security-requirements)). The one default that is not deny-by-default is **unrestricted access mode when the mode is omitted**, which is documented, and writes still require an explicit `read_only=false`. | | Complete mediation | Every statement passes `SqlSession`, every connection passes `DatabaseRegistry` and its lock check, and every result passes the row/byte/cell budget and the response byte cap. PostgreSQL mediates object access on every statement. | | Open design | The source, protocol, limits and weaknesses are public ([SECURITY.md](../SECURITY.md)); nothing relies on secrecy of the design. Secrets are only the operator's credentials, kept outside the repository. | | Separation of privilege | Writing requires three independent conditions: unrestricted mode, an explicit `read_only=false` and PostgreSQL privileges. Releases need a main-branch dispatch plus a 2FA approval for npm. | @@ -106,8 +106,8 @@ The eight principles of [Saltzer and Schroeder](https://web.mit.edu/Saltzer/www/ | [Improper input validation (CWE-20)](https://cwe.mitre.org/data/definitions/20.html) | Countered | R6; fixed diagnostics for parse failures. | | [Sensitive information in error messages and logs (CWE-209](https://cwe.mitre.org/data/definitions/209.html), [CWE-532)](https://cwe.mitre.org/data/definitions/532.html) | Countered | R4; verified by confidentiality scenarios. | | [Hard-coded credentials (CWE-798)](https://cwe.mitre.org/data/definitions/798.html) | Countered | None in the repository; Gitleaks with PostgreSQL URI/Npgsql rules scans all history; GitHub push protection is enabled. | -| [Cleartext storage or transmission of credentials (CWE-312](https://cwe.mitre.org/data/definitions/312.html), [CWE-319)](https://cwe.mitre.org/data/definitions/319.html) | Partly | The server never persists credentials. Environment values and files are plaintext by nature and documented as such; transmission security depends on `SSL Mode` (see N5). | -| [Improper certificate validation (CWE-295)](https://cwe.mitre.org/data/definitions/295.html) | Not countered by default | Operator-controlled; warning only. | +| [Cleartext storage or transmission of credentials (CWE-312](https://cwe.mitre.org/data/definitions/312.html), [CWE-319)](https://cwe.mitre.org/data/definitions/319.html) | Partly | The server never persists credentials. Environment values and files are plaintext by nature and documented as such; remote connections verify TLS unless the operator sets an explicit `SSL Mode` (see N5). | +| [Improper certificate validation (CWE-295)](https://cwe.mitre.org/data/definitions/295.html) | Countered by default | R7: chain and host name are verified unless the operator sets an explicit unverified `SSL Mode`, which is warned about. | | [Uncontrolled resource consumption (CWE-400)](https://cwe.mitre.org/data/definitions/400.html) | Countered for the server; not for PostgreSQL | R5. | | [OS command injection (CWE-78)](https://cwe.mitre.org/data/definitions/78.html) | Not applicable | `src/` starts no processes. Build and release tools start processes through one shared runner with explicit arguments and timeouts. | | [Path traversal (CWE-22)](https://cwe.mitre.org/data/definitions/22.html) | Not applicable | The only file read is the operator-supplied targets file path; no tool argument names a file. | @@ -124,7 +124,7 @@ Memory-safety and undefined-behavior hardening come from the language: C# is mem These are recorded here rather than argued away. Items marked **gap** are the ones the project would change if it could. -1. **Gap: TLS defaults.** The server sets no `SSL Mode`. With Npgsql's default `Prefer`, the connection is encrypted only if the server offers TLS, and the certificate is not verified, so an on-path attacker can impersonate PostgreSQL and capture the login or downgrade to plaintext. The server warns on stderr for non-loopback hosts without `VerifyCA`/`VerifyFull` but does not refuse them. Operators should use `SSL Mode=VerifyFull`. This is why the OpenSSF criteria `crypto_certificate_verification`, `crypto_verification_private` and `crypto_used_network` are answered Unmet ([security posture](security-posture.md#openssf-best-practices-enrollment)). Verification by default for non-loopback hosts would be a behavior change needing an upgrade note. +1. **Operator opt-out of TLS verification.** An explicit `SSL Mode` of `Prefer`, `Allow`, `Require` or `Disable` is honored unchanged and, for a remote host, only warned about on stderr, so an on-path attacker can then impersonate PostgreSQL. A connection string that omits `SSL Mode` is verified by default (the release after 0.5.0). Loopback and Unix-socket profiles default to `Disable` because no network attacker is in play there; a loopback port forwarded to a remote server that requires TLS needs an explicit `SSL Mode`. Weak TLS protocol versions and cipher suites are whatever .NET and the PostgreSQL server negotiate. 2. **Legacy authentication.** Npgsql still answers a server's legacy MD5 request unless the operator sets `Require Auth` accordingly. 3. **Lock check is a snapshot.** Grants, roles and extensions changed after startup are enforced only by the in-process lock until the next start or `--validate`. 4. **The guard is a lexer.** Its first-keyword denylist does not cover `set_config()` or privileged routines. Isolation relies on PostgreSQL. diff --git a/docs/roadmap.md b/docs/roadmap.md index 3f7a348..1e45677 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -13,7 +13,6 @@ This is the direction for the twelve months from 2026-10-11 to 2027-10-11, so th ### Close the security gaps the project already knows about -- **TLS defaults.** Npgsql's default `SSL Mode=Prefer` encrypts without verifying the server and can fall back to plaintext. Today the server only warns ([assurance case, residual risks](assurance-case.md#7-residual-risks-and-known-gaps)). The intent is to decide, with an upgrade note, whether connections to non-loopback hosts should require certificate verification unless the operator opts out explicitly. - **Measured test coverage.** Statement and branch coverage of the integration run is not measured today. The intent is to collect it with a .NET FLOSS tool in CI, publish the result and then set a threshold. - **Reproducible packages.** Archives and npm tarballs are byte-identical across rebuilds; the `.nupkg` and the SBOM are not. The intent is to make them deterministic. - **Release provenance visibility.** Releases from 0.5.0 carry `provenance.sigstore.json`, which is how Scorecard's Signed-Releases check sees the signatures. Each new release extends that coverage. From e5393424d14af405f502f0c7bce3f83c93953936 Mon Sep 17 00:00:00 2001 From: AG <288536783+codegiveness@users.noreply.github.com> Date: Sun, 11 Oct 2026 09:59:51 +0700 Subject: [PATCH 09/12] docs: mark the engineered Silver criteria met and record the remaining gaps build_repeatable, test_statement_coverage80, crypto_certificate_verification and crypto_verification_private are proposed Met from this branch's changes; coverage figures come from the integrated branch (89.41% lines, 84.84% branches). access_continuity remains the only Silver MUST gap. --- .bestpractices.json | 32 ++++++++++++++++---------------- CHANGELOG.md | 2 +- docs/assurance-case.md | 3 ++- docs/roadmap.md | 4 ++-- docs/security-posture.md | 13 ++++++------- 5 files changed, 27 insertions(+), 27 deletions(-) diff --git a/.bestpractices.json b/.bestpractices.json index 9a10d6d..a6f7caa 100644 --- a/.bestpractices.json +++ b/.bestpractices.json @@ -105,8 +105,8 @@ "crypto_pfs_justification": "Public-source evidence: the only key-agreement protocol is TLS, implemented by .NET SslStream and the operating system's TLS provider with OS-selected protocols and suites: https://github.com/npgsql/npgsql/blob/d3768398c17877b3a916c3c4d87e8e11698991fc/src/Npgsql/Internal/NpgsqlConnector.cs#L1229-L1239 . Full TLS 1.3 handshakes, used when the OS and the operator's PostgreSQL server support TLS 1.3, always use ephemeral (EC)DHE key exchange and so provide forward secrecy; with TLS 1.2, forward secrecy depends on the OS policy and the server preferring ECDHE or DHE suites. TLS use, certificate validation and server TLS settings are operator-controlled, and the server does not force TLS: https://github.com/codegiveness/postgresql-sharp-mcp/blob/fbbe5aa0c2abea5d48d58fa282d84204921c8e19/SECURITY.md#connection-and-resource-settings . SCRAM authentication is not a key-agreement protocol. This is not a guarantee for every deployment.", "crypto_random_status": "Met", "crypto_random_justification": "Public-source evidence: the application generates no cryptographic keys or nonces itself and contains no cryptographic or random-number APIs: https://github.com/codegiveness/postgresql-sharp-mcp/tree/fbbe5aa0c2abea5d48d58fa282d84204921c8e19/src . Npgsql 10.0.3 generates the SCRAM client nonce with System.Security.Cryptography.RandomNumberGenerator, a cryptographically secure generator: https://github.com/npgsql/npgsql/blob/d3768398c17877b3a916c3c4d87e8e11698991fc/src/Npgsql/Internal/NpgsqlConnector.Auth.cs#L194-L201 . TLS session keys and nonces are generated by the operating system's TLS provider used by .NET SslStream: https://github.com/npgsql/npgsql/blob/d3768398c17877b3a916c3c4d87e8e11698991fc/src/Npgsql/Internal/NpgsqlConnector.cs#L1229-L1239 . Neither the application nor Npgsql uses System.Random for these values.", - "test_most_status": "Unmet", - "test_most_justification": "Public-source evidence: the repository measures no statement or branch coverage, so it cannot show that the suite covers most code branches. The integration verifier exercises MCP tool calls, configuration inputs, permissions/RLS, target isolation, rollback, bounded results, sanitized errors and installed package entrypoints against disposable PostgreSQL: https://github.com/codegiveness/postgresql-sharp-mcp/blob/fbbe5aa0c2abea5d48d58fa282d84204921c8e19/CONTRIBUTING.md#verify-behavior . CI passed on main revision fbbe5aa0c2abea5d48d58fa282d84204921c8e19: https://github.com/codegiveness/postgresql-sharp-mcp/actions/runs/38052409295 . FsCheck properties and the libFuzzer campaign add varied SQL inputs. This shows breadth of functional and input testing, not measured branch coverage; Met would require a coverage measurement.", + "test_most_status": "Met", + "test_most_justification": "Met. The integration scenarios exercise MCP tool calls, configuration inputs, permissions/RLS, target isolation, rollback, bounded results, TLS policy and sanitized errors against disposable PostgreSQL; measured with coverlet on 2026-10-11 they reach 89.41% of statements and 84.84% of branches in the shipped assemblies, and CI fails below 80% line coverage: https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/CONTRIBUTING.md#measure-test-coverage . These files are changed by the pull request that proposes these answers; the URLs show the described state once that change is merged to main.", "contribution_status": "Met", "contribution_justification": "Public-source evidence: CONTRIBUTING explains the contribution process: actionable bugs and enhancements use GitHub issues, changes are submitted as pull requests that describe contract, compatibility and security impact plus the commands exercised, and reviews report actionable findings; main accepts changes only through pull requests with passing required checks: https://github.com/codegiveness/postgresql-sharp-mcp/blob/fbbe5aa0c2abea5d48d58fa282d84204921c8e19/CONTRIBUTING.md#issues-pull-requests-and-reviews", "floss_license_status": "Met", @@ -179,8 +179,8 @@ "build_preserve_debug_justification": "SHOULD criterion. Source builds with dotnet build keep the SDK's debug-information defaults and the project sets no override (https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/Directory.Build.props ). The release packaging tool, however, forces DebugType=None and DebugSymbols=false for the shipped archives and packages and rejects archives containing .pdb files (https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/tools/PostgreSqlMcp.Build/Program.cs ), so debug information cannot be requested for release artifacts.", "build_non_recursive_status": "Met", "build_non_recursive_justification": "The build is an MSBuild solution whose projects declare explicit ProjectReference dependencies (Core <- Tools <- App); no recursive make or per-directory build scripts exist: https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/postgresql-sharp-mcp.slnx and https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/CONTRIBUTING.md#build-from-source", - "build_repeatable_status": "Unmet", - "build_repeatable_justification": "Partly met. Two clean Release builds on 2026-10-11 produced identical hashes for the application assemblies, and release archives and npm tarballs are byte-identical across rebuilds (CHANGELOG 0.5.0, covered by the release-regressions verifier command). The .nupkg (zip entry timestamps) and the CycloneDX SBOM (random serial number, timestamp) are not bit-for-bit repeatable, so the criterion is not met for every release output: https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/CONTRIBUTING.md#automation-and-releases . Making them deterministic would change this.", + "build_repeatable_status": "Met", + "build_repeatable_justification": "Met. Rebuilding the same source produces byte-identical release outputs: the five archives, the six npm tarballs, the .nupkg (NuGet's DeterministicTimestamp fixes every entry time at 1980-01-01) and the CycloneDX SBOM (serial number derived from the content, timestamp from the release commit time), and therefore SHA256SUMS. On 2026-10-11 two clean rebuilds under different time zones gave identical hashes for all of them. The offline release-regressions verifier (repeated dotnet pack of a probe project and SBOM normalization) and the packages verifier (fixed .nupkg entry times) enforce this; the only per-run asset is the signed provenance attestation bundle: https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/CONTRIBUTING.md#automation-and-releases . These files are changed by the pull request that proposes these answers; the URLs show the described state once that change is merged to main.", "installation_common_status": "Met", "installation_common_justification": "Installation and uninstallation use commonly used package managers: npm (npx, npm install -g/uninstall) and the .NET tool (dotnet tool install/uninstall), plus verified archives and a Dockerfile: https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/README.md#1-install-or-run and https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/README.md#other-installation-methods", "installation_standard_variables_status": "N/A", @@ -199,8 +199,8 @@ "automated_integration_testing_justification": "The CI workflow runs the real PostgreSQL integration suite and package installation checks on every push to main and every pull request and reports success or failure per job: https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/.github/workflows/ci.yml . Example: https://github.com/codegiveness/postgresql-sharp-mcp/actions/runs/38102103515 succeeded on 2026-10-11.", "regression_tests_added50_status": "Met", "regression_tests_added50_justification": "Checked with git history on 2026-10-11: 9 of the 11 fix: commits on main (all since the repository was created on 2026-10-02) changed the automated test scenarios, either the C# verifier in tools/PostgreSqlMcp.Verify (pull requests 11, 25, 45, 46, 49, 51 and 60) or the earlier smoke script it replaced (two earlier fixes). The other two changed release workflows, release tooling and documentation only. The contribution policy requires regression coverage for fixes: https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/CONTRIBUTING.md#verify-behavior . The window is nine days, so this is a small sample and changed scenarios are not individually proven to be regression tests for each bug.", - "test_statement_coverage80_status": "Unmet", - "test_statement_coverage80_justification": "Statement coverage is not measured. The verifier exercises real MCP calls against PostgreSQL 13 to 18, but no coverage tool is run in CI, so 80% cannot be claimed. Measuring coverage of the integration run with a .NET FLOSS tool and reaching the threshold would change this; see https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/docs/roadmap.md . This file is added by the pull request that proposes these answers; its URL resolves once that change is merged to main.", + "test_statement_coverage80_status": "Met", + "test_statement_coverage80_justification": "Met. The verifier's coverage command runs the integration scenarios against a disposable PostgreSQL with the FLOSS coverlet console tool instrumenting the three shipped assemblies, writes a Cobertura report and fails below 80% total line (statement) coverage; the CI verify job runs it and uploads the report as the coverage artifact. The first measurement on 2026-10-11 was 89.41% lines (1073/1200), 84.84% branches and 98.38% methods; only compiler-generated [GeneratedRegex] code is excluded: https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/CONTRIBUTING.md#measure-test-coverage . These files are changed by the pull request that proposes these answers; the URLs show the described state once that change is merged to main.", "test_policy_mandated_status": "Met", "test_policy_mandated_justification": "CONTRIBUTING and AGENTS.md require consumer-visible automated coverage for major new functionality and for plausible consumer-visible regressions, extending the maintained verifier: https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/CONTRIBUTING.md#verify-behavior and https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/AGENTS.md#implementation-and-evidence", "implement_secure_design_status": "Met", @@ -209,14 +209,14 @@ "crypto_algorithm_agility_justification": "The project implements no cryptography; algorithm and protocol selection is delegated to Npgsql, .NET and the operating system's TLS and to the PostgreSQL server (SCRAM-SHA-256, other methods, TLS versions and cipher suites), so operators can switch algorithms through server, OS or connection-string settings without code changes: https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/docs/assurance-case.md#3-trust-boundaries and https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/SECURITY.md#connection-and-resource-settings", "crypto_credential_agility_status": "Met", "crypto_credential_agility_justification": "Credentials are never hard-coded: they come from the environment, an owner-protected targets file or CLI options, separate from other configuration, and can be replaced by restarting the server without recompiling: https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/README.md#2-prepare-the-connection-environment and https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/SECURITY.md#credentials-and-diagnostics", - "crypto_used_network_status": "Unmet", - "crypto_used_network_justification": "The server's only network communication is the PostgreSQL connection, and TLS is supported but not required: the server sets no SSL Mode, and Npgsql's default Prefer encrypts only if the server offers TLS and can fall back to unencrypted (https://www.npgsql.org/doc/security.html , observed 2026-10-11). The server prints a warning for non-loopback hosts without VerifyCA or VerifyFull but does not refuse them: https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/docs/assurance-case.md#7-residual-risks-and-known-gaps . Insecure transport is therefore not disabled by default. Defaulting non-loopback connections to required, verified TLS (a behavior change needing an upgrade note) would change this.", + "crypto_used_network_status": "Met", + "crypto_used_network_justification": "Met. The server's only network protocol is the PostgreSQL connection, and by default it is encrypted: a profile without SSL Mode uses VerifyFull for every non-loopback host, so a remote server that offers no TLS is refused with tls_unavailable instead of falling back to plaintext. Unencrypted connections happen only to loopback/Unix-socket hosts or when the operator explicitly sets SSL Mode=Disable/Allow/Prefer, which is warned about on stderr for remote hosts: https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/SECURITY.md#connection-and-resource-settings . These files are changed by the pull request that proposes these answers; the URLs show the described state once that change is merged to main.", "crypto_tls12_status": "Met", "crypto_tls12_justification": "TLS is performed by Npgsql through .NET SslStream; the server does not restrict protocol versions and leaves negotiation to the operating system, which on supported platforms offers TLS 1.2 and later: https://learn.microsoft.com/dotnet/core/extensions/sslstream-best-practices and https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/SECURITY.md#connection-and-resource-settings . Whether TLS is used at all is the operator's SSL Mode choice (see crypto_used_network).", - "crypto_certificate_verification_status": "Unmet", - "crypto_certificate_verification_justification": "Not by default. With Npgsql's default SSL Mode=Prefer, and with Require, TLS is used without certificate verification (https://www.npgsql.org/doc/security.html , observed 2026-10-11); verification happens only when the operator sets VerifyCA or VerifyFull, which the README examples and warnings recommend: https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/SECURITY.md#connection-and-resource-settings and https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/docs/assurance-case.md#7-residual-risks-and-known-gaps . Defaulting remote connections to VerifyFull would change this.", - "crypto_verification_private_status": "Unmet", - "crypto_verification_private_justification": "Because certificate verification is not the default (see crypto_certificate_verification), the login can be sent to an unverified server under the default SSL Mode, as SECURITY.md already warns. With SSL Mode=VerifyCA or VerifyFull Npgsql verifies the certificate during the TLS handshake, before authentication. See https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/SECURITY.md#connection-and-resource-settings .", + "crypto_certificate_verification_status": "Met", + "crypto_certificate_verification_justification": "Met. A connection profile that omits SSL Mode gets SSL Mode=VerifyFull for any non-loopback host (certificate chain and host name verified, no plaintext or unverified fallback) and Disable only for loopback and Unix sockets; this is applied to every profile at startup. An explicit unverified mode is the operator's documented opt-out and is warned about on stderr for remote hosts. The verifier's TLS scenarios against a disposable throwaway-CA PostgreSQL show that an untrusted certificate is refused with tls_verification_failed, a configured Root Certificate connects, and a trusted chain with a host-name mismatch is rejected: https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/SECURITY.md#connection-and-resource-settings and https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/README.md#connection-string-quoting-and-tls . These files are changed by the pull request that proposes these answers; the URLs show the described state once that change is merged to main.", + "crypto_verification_private_status": "Met", + "crypto_verification_private_justification": "Met. Under the default (VerifyFull for non-loopback hosts) Npgsql verifies the server certificate during the TLS handshake, before the startup packet (user name) or any password is sent. The verifier's TLS scenario asserts that after a failed verification the PostgreSQL server log contains no login attempt for the configured role. Operators who explicitly set Prefer, Allow, Require or Disable opt out; that opt-out is documented and warned about for remote hosts: https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/SECURITY.md#connection-and-resource-settings . These files are changed by the pull request that proposes these answers; the URLs show the described state once that change is merged to main.", "signed_releases_status": "Met", "signed_releases_justification": "Releases from v0.2.0 carry GitHub build attestations (Sigstore keyless signatures bound to the release.yml workflow identity and the exact source commit) for every archive, package, the SBOM and SHA256SUMS; since v0.5.0 the signed bundle is also published as the provenance.sigstore.json release asset (gh release view v0.5.0 listed it on 2026-10-11). No long-lived private signing key exists on the distribution sites. The documented verification process, which checks repository, signer workflow and subject digest with gh attestation verify, is in https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/docs/security-posture.md#supply-chain-evidence (the release verification commands near the end of that section). v0.1.0 predates attestations.", "version_tags_signed_status": "Unmet", @@ -240,11 +240,11 @@ "two_person_review_status": "Unmet", "two_person_review_justification": "Every human change is authored by the sole maintainer, and GitHub does not let an author approve their own pull request; the main ruleset requires zero approvals. Scorecard reported 0 of 27 approved changesets on 2026-10-11. A different person must review at least half of proposed modifications to change this.", "build_reproducible_status": "Unmet", - "build_reproducible_justification": "Release archives and npm tarballs are byte-identical across rebuilds, but the .nupkg and the SBOM are not (see build_repeatable); the criterion requires a reproducible build for the project's results. Deterministic .nupkg and SBOM output would change this.", + "build_reproducible_justification": "Not yet shown for every release output. On 2026-10-11 an independent local rebuild of the v0.5.0 source produced a linux-x64 npm tarball with the same SHA-256 as the asset built by the release workflow, and the archives and npm tarballs are documented as reproducible. The .nupkg and SBOM became deterministic only after v0.5.0 (see build_repeatable), so no published release yet shows them reproduced independently. Rebuilding the next release locally and matching every SHA256SUMS entry except the per-run provenance bundle would change this.", "test_statement_coverage90_status": "Unmet", - "test_statement_coverage90_justification": "Statement coverage is not measured (see test_statement_coverage80).", - "test_branch_coverage80_status": "Unmet", - "test_branch_coverage80_justification": "Branch coverage is not measured (see test_statement_coverage80).", + "test_statement_coverage90_justification": "Not met: total statement coverage measured on 2026-10-11 was 89.41%, below 90%. The least-covered file is src/PostgreSqlMcp/Program.cs (the --help, --version and invalid --log-level paths); a scenario for those paths would change this. See test_statement_coverage80 and https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/CONTRIBUTING.md#measure-test-coverage .", + "test_branch_coverage80_status": "Met", + "test_branch_coverage80_justification": "Met. Total branch coverage of the shipped assemblies under the integration scenarios was 84.84% (907/1069) on 2026-10-11, measured with the FLOSS coverlet console tool by the verifier's coverage command, which CI runs on every pull request (its gate is on line coverage; branch coverage is reported alongside): https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/CONTRIBUTING.md#measure-test-coverage . These files are changed by the pull request that proposes these answers; the URLs show the described state once that change is merged to main.", "hardened_site_status": "Met", "hardened_site_justification": "The repository, issue tracker and release downloads are served by github.com, which sent Strict-Transport-Security, Content-Security-Policy, X-Content-Type-Options: nosniff and X-Frame-Options: deny in responses to curl -I on 2026-10-11 for https://github.com/codegiveness/postgresql-sharp-mcp and the v0.5.0 release asset URL (the asset itself is then served from a GitHub-controlled redirect target that was not separately checked). The package registries are third-party sites: npmjs.com returned HSTS, a CSP, nosniff and X-Frame-Options; the nuget.org package page returned HSTS, nosniff and X-Frame-Options but its CSP was report-only.", "general_comments": "Evidence-backed project metadata for https://www.bestpractices.dev/en/projects/15155 . Proposed answers require review and saving on the badge website. Silver and gold proposals were added on 2026-10-11 and cite files that exist only after the pull request that adds them is merged. Not inferred and therefore not proposed: require_2FA and secure_2FA (the GitHub account's 2FA method is not observable from the repository) and security_review (a human review of docs/assurance-case.md must be recorded in that document first). Also not inferred: developer knowledge beyond the owner's attestation; response timeliness for reports not yet received (0 issues, security advisories and Dependabot alerts and 0 open CodeQL alerts as checked through the GitHub API on 2026-10-10 and again on 2026-10-11); negotiated TLS key lengths, algorithms or forward secrecy beyond Npgsql, .NET and OS defaults and the operator's SSL Mode and PostgreSQL server settings; absence of vulnerabilities or credentials beyond the cited point-in-time scans; branch and statement coverage; and pre-release static analysis for v0.1.0, which predates the CodeQL workflow." diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ded7de..63faa6e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ Upgrade note: connections to non-loopback hosts now verify the server certificat - SECURITY: document how vulnerability reports are handled. - Propose Silver and Gold Best Practices answers in `.bestpractices.json`, and record the live Scorecard figures, the reference project's figures, the criteria that remain unmet and an owner checklist in [docs/security-posture.md](docs/security-posture.md). Nothing is saved on bestpractices.dev until the owner reviews it. - Make the `.nupkg` and the CycloneDX SBOM reproducible, so every release asset except the per-run provenance bundle is byte-identical across rebuilds of the same source, and `SHA256SUMS` with them. `tools/PostgreSqlMcp.Build` passes NuGet's supported `DeterministicTimestamp` property to `dotnet pack`, fixing every zip entry time at 1980-01-01 (previously the wall clock; the entry order, contents and `nuget.psmdcp` name were already stable, so published package layouts are unchanged). `release-tool sbom` rewrites dotnet-CycloneDX's random `serialNumber` and wall-clock `metadata.timestamp` (the generator can only omit the serial number and cannot fix the timestamp) to a version 8 UUID derived from the SHA-256 of the BOM and the checked-out commit's time; the BOM stays valid CycloneDX JSON with the same properties and component order. `release-regressions` now proves both offline (SBOM normalization and two `dotnet pack` runs of a probe project more than two seconds apart), and `packages` rejects a `.nupkg` entry that carries a build-time stamp. The new `.nupkg` entry timestamps and SBOM fields apply to the next release; already published versions are unchanged. -- Verifier and CI: measure statement coverage of the real server under the integration scenarios with the pinned FLOSS coverlet console tool. `tools/PostgreSqlMcp.Verify -- coverage [--output dir] [--threshold n]` instruments the three shipped assemblies, runs `integration --no-build` (new flag that skips the build step) against the disposable Docker fixture, prints per-assembly line, branch and method coverage with the files and methods the scenarios never reach, and writes a Cobertura report. The CI `verify` job runs it with a gate of 80% total line coverage and uploads the report as the `coverage` artifact. The first measurement was 89.81% lines, 84.14% branches and 98.33% methods in total, excluding only compiler-generated `[GeneratedRegex]` code. It is a test-infrastructure change: no shipped runtime behavior changes. +- Verifier and CI: measure statement coverage of the real server under the integration scenarios with the pinned FLOSS coverlet console tool. `tools/PostgreSqlMcp.Verify -- coverage [--output dir] [--threshold n]` instruments the three shipped assemblies, runs `integration --no-build` (new flag that skips the build step) against the disposable Docker fixture, prints per-assembly line, branch and method coverage with the files and methods the scenarios never reach, and writes a Cobertura report. The CI `verify` job runs it with a gate of 80% total line coverage and uploads the report as the `coverage` artifact. The first measurement was 89.41% lines, 84.84% branches and 98.38% methods in total, excluding only compiler-generated `[GeneratedRegex]` code. It is a test-infrastructure change: no shipped runtime behavior changes. ## 0.5.0 diff --git a/docs/assurance-case.md b/docs/assurance-case.md index a36eceb..d29e6ef 100644 --- a/docs/assurance-case.md +++ b/docs/assurance-case.md @@ -131,7 +131,7 @@ These are recorded here rather than argued away. Items marked **gap** are the on 5. **Prompt injection** cannot be detected by the server (N7). 6. **Plaintext secrets at rest in process state.** Environment values, process arguments and targets files are exposed to sufficiently privileged local users; the server does not check targets-file permissions. 7. **Container image.** The Dockerfile installs a few runtime libraries without version pins; Trivy scans the built image on every run and fails on HIGH/CRITICAL findings. -8. **Test coverage is not measured.** The verifier exercises real behavior against PostgreSQL 13–18, but statement and branch coverage have not been computed, so untested paths cannot be enumerated. +8. **Coverage is incomplete.** The `integration` scenarios reach 89.41% of statements and 84.84% of branches in the shipped assemblies (coverlet, 2026-10-11, PostgreSQL 17); CI fails below 80% lines. The [coverage command](../CONTRIBUTING.md#measure-test-coverage) lists the files and methods no scenario reaches. 9. **Single maintainer.** Review is self-review; there is no independent approval and no continuity if the maintainer is unavailable ([governance](../GOVERNANCE.md#continuity-if-the-maintainer-is-unavailable), [security posture](security-posture.md)). ## 8. Re-verifying the evidence @@ -142,6 +142,7 @@ dotnet run --project tools/PostgreSqlMcp.Verify -c Release -- integration dotnet run --project tools/PostgreSqlMcp.Verify -c Release -- sarif-regressions dotnet run --project tools/PostgreSqlMcp.Verify -c Release -- release-regressions dotnet run --project tools/PostgreSqlMcp.Fuzz -c Release -p:RestoreLockedMode=true -- campaign 60 +dotnet run --project tools/PostgreSqlMcp.Verify -c Release -- coverage --threshold 80 # needs Docker ``` Hosted-run claims are tied to the run URLs above and to the revision they ran on; check the current `main` results rather than assuming they still hold. diff --git a/docs/roadmap.md b/docs/roadmap.md index 1e45677..c3e2fd9 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -13,8 +13,8 @@ This is the direction for the twelve months from 2026-10-11 to 2027-10-11, so th ### Close the security gaps the project already knows about -- **Measured test coverage.** Statement and branch coverage of the integration run is not measured today. The intent is to collect it with a .NET FLOSS tool in CI, publish the result and then set a threshold. -- **Reproducible packages.** Archives and npm tarballs are byte-identical across rebuilds; the `.nupkg` and the SBOM are not. The intent is to make them deterministic. +- **Statement coverage above 90%.** The integration scenarios reach 89.41% of statements and 84.84% of branches (measured 2026-10-11; CI fails below 80% lines, see [Measure test coverage](../CONTRIBUTING.md#measure-test-coverage)). The least-covered file is `src/PostgreSqlMcp/Program.cs`: a scenario for `--help`, `--version` and an invalid `--log-level` would close most of the gap to 90%. +- **Independently reproduced release.** Every release asset except the per-run provenance bundle is now byte-identical across rebuilds of the same source. The next release is the first whose `.nupkg` and SBOM can be checked against an independent rebuild. - **Release provenance visibility.** Releases from 0.5.0 carry `provenance.sigstore.json`, which is how Scorecard's Signed-Releases check sees the signatures. Each new release extends that coverage. - **Assurance case upkeep.** Review and update the assurance case whenever the trust boundary, a default or a release path changes, and at least once a year. diff --git a/docs/security-posture.md b/docs/security-posture.md index c0b51c4..a865b4b 100644 --- a/docs/security-posture.md +++ b/docs/security-posture.md @@ -190,12 +190,12 @@ The repository was created on 2026-10-02; activity does not supply months of mai On 2026-10-11 the [project JSON](https://www.bestpractices.dev/projects/15155.json) still reported `badge_level: passing`, passing 100%, silver 15%, gold 22% and `updated_at` 2026-10-10 13:37:58 UTC, so no silver or gold answer has been saved. The criteria are listed at [silver](https://www.bestpractices.dev/en/criteria/1) and [gold](https://www.bestpractices.dev/en/criteria/2); several criteria appear at both levels and share one answer field, so a single answer applies to both. -[`.bestpractices.json`](../.bestpractices.json) now also proposes answers for 57 silver and gold criteria: 36 Met, 17 Unmet and 4 N/A. Each Met answer cites a file, a command result or a dated API observation; files added by the same pull request resolve only after it merges. The proposals are not a certification, and nothing is saved on the hosted entry until the owner reviews each answer and saves it. Not proposed, because the repository cannot establish them: +[`.bestpractices.json`](../.bestpractices.json) now also proposes answers for 57 silver and gold criteria: 42 Met, 11 Unmet and 4 N/A. Each Met answer cites a file, a command result or a dated API observation; files added by the same pull request resolve only after it merges. The proposals are not a certification, and nothing is saved on the hosted entry until the owner reviews each answer and saves it. Not proposed, because the repository cannot establish them: - `require_2FA` and `secure_2FA`: whether the maintainer's GitHub account uses two-factor authentication, and with which method, is not observable from the repository (the API returned no value for the authenticated account on 2026-10-11). GitHub began requiring 2FA from code contributors on github.com in 2023 ([GitHub announcement](https://github.blog/news-insights/product-news/raising-the-bar-for-software-security-github-2fa-begins-march-13/)), but the answer is the owner's attestation. - `security_review`: the criterion needs a human review of the security requirements and boundary. [docs/assurance-case.md](assurance-case.md#review-record) is a draft by an AI agent and has a pending maintainer row; record the review there first. -Silver still fails on five MUST criteria (`access_continuity`, `build_repeatable`, `test_statement_coverage80`, `crypto_certificate_verification`, `crypto_verification_private`), so `achieve_silver` and the Scorecard CII-Best-Practices score stay at passing until they change. Gold additionally needs the Unmet criteria in the next table. The silver and gold answers describe 2026-10-11 and must be re-evaluated when reports, releases or settings change. +Silver still fails on one MUST criterion, `access_continuity`, so `achieve_silver` and the Scorecard CII-Best-Practices score stay at passing until it changes and the owner saves the answers. Four other Silver MUST criteria were Unmet earlier on 2026-10-11 and are proposed as Met by engineering changes in the same pull request: `build_repeatable` (deterministic `.nupkg` and SBOM), `test_statement_coverage80` (coverlet measurement: 89.41% statements, 84.84% branches, CI gate at 80%), and `crypto_certificate_verification` and `crypto_verification_private` (verified TLS by default, a breaking change with an upgrade note). Gold additionally needs the Unmet criteria in the next table. The silver and gold answers describe 2026-10-11 and must be re-evaluated when reports, releases or settings change. #### What stays unmet and what would change it @@ -208,9 +208,8 @@ Silver still fails on five MUST criteria (`access_continuity`, `build_repeatable | Scorecard Branch-Protection above 4 | Check | Required approvers, code-owner review and last-push approval are off because they would block a sole maintainer | Enable them only once a second reviewer exists | | Scorecard Maintained | Check | Repository younger than 90 days | Time: age-eligible 2026-12-31, then normal activity | | Scorecard Signed-Releases above 8 | Check | The GitHub bundle is not `.intoto.jsonl` | A tag-referenced SLSA generator, rejected by the full-SHA pin policy. Reaching 8 needs bundles on the five newest releases | -| `test_statement_coverage80`, `test_statement_coverage90`, `test_branch_coverage80` | Silver MUST, Gold MUST | No coverage is measured | Run a FLOSS .NET coverage collector over the integration run in CI, publish the figures, and add tests until they reach 80% statement, 90% statement and 80% branch | -| `build_repeatable`, `build_reproducible` | Silver MUST, Gold MUST | `.nupkg` zip timestamps and the SBOM's random serial number and timestamp differ per build | Deterministic `.nupkg` and SBOM output, verified by comparing two rebuilds in `release-regressions` | -| `crypto_certificate_verification`, `crypto_verification_private`, `crypto_used_network` | Silver MUST, MUST, SHOULD (Gold MUST for the last) | Npgsql's default `SSL Mode=Prefer` does not verify the server and can fall back to plaintext; the server only warns | A behavior change, with an upgrade note, so non-loopback connections require verified TLS unless the operator opts out | +| `test_statement_coverage90` | Gold MUST | 89.41% statement coverage measured on 2026-10-11 (`test_statement_coverage80` and `test_branch_coverage80` are met at 89.41% and 84.84%) | A scenario for the `--help`, `--version` and invalid `--log-level` paths of `src/PostgreSqlMcp/Program.cs` (53 of its 105 lines are unexecuted), or other real scenarios from the `coverage` command's unexecuted list, until the total passes 90% | +| `build_reproducible` | Gold MUST | Every asset except the provenance bundle is byte-identical across same-toolchain rebuilds, but the deterministic `.nupkg` and SBOM have not shipped in a release yet | After the next release, rebuild it independently and match every `SHA256SUMS` entry except `provenance.sigstore.json` | | `version_tags_signed` | Silver SUGGESTED | Tags are annotated but unsigned (v0.3.3 is lightweight) | Sign release tags (`git tag -s` with a signing key registered on GitHub) and document `git tag -v` | | `small_tasks` | Gold MUST | No open issue carries `good first issue` or `help wanted` | Label real, small issues | | `dco` | Silver SHOULD | No sign-off policy | Adopt and enforce a DCO or contributor agreement | @@ -222,7 +221,7 @@ Silver still fails on five MUST criteria (`access_continuity`, `build_repeatable Nothing here has been done by the change that proposes it; each item changes a remote setting or a published release and needs the owner. -1. **Review and merge the pull request** that adds `GOVERNANCE.md`, `docs/architecture.md`, `docs/roadmap.md`, `docs/assurance-case.md`, the coding and review standards, the SPDX headers, `.editorconfig` with its CI step and the answer proposals. The proposal evidence URLs resolve only after the merge. +1. **Review and merge the pull request** that adds `GOVERNANCE.md`, `docs/architecture.md`, `docs/roadmap.md`, `docs/assurance-case.md`, the coding and review standards, the SPDX headers, `.editorconfig` with its CI step, deterministic `.nupkg`/SBOM output, the coverlet coverage gate, verified TLS by default (breaking) and the answer proposals. The proposal evidence URLs resolve only after the merge. 2. **Read `docs/assurance-case.md` and record your review** in its review record. Confirm or correct the roadmap and governance statements, which are drafts written for the maintainer's approval. 3. **Save the answers on bestpractices.dev.** Sign in as the owner and open the [silver](https://www.bestpractices.dev/en/projects/15155/silver/edit) and [gold](https://www.bestpractices.dev/en/projects/15155/gold/edit) sections. Use **Save (and continue) 🤖** so the site loads the repository proposals from `.bestpractices.json`, review every proposed answer against the evidence, and change any you disagree with. Answer `require_2FA`, `secure_2FA` and `security_review` yourself. 4. **Attach the existing provenance bundles to the older releases in Scorecard's window** (v0.3.1, v0.3.2, v0.3.3 and v0.4.0; v0.2.0 is already outside the window, and v0.1.0 has no attestation). Editing a published release is reserved for the owner. On 2026-10-11 the read-only first steps below were run for v0.4.0: its attestation was retrieved as one Sigstore bundle holding one SLSA v1 statement with nine subjects. The verification and upload steps were not run, and the `gh` version used there has no `gh attestation` command, so use a current one: @@ -240,7 +239,7 @@ Nothing here has been done by the change that proposes it; each item changes a r Upload only after verification passes, and repeat for the other three tags. Check that the bundle's subjects match that release's asset digests first. 5. **Label real small tasks.** Open or label two or three small issues `good first issue` (for example, add a direct `SqlGuard` verifier scenario, make constructor style consistent in `DatabaseTools`, or document the `ConfigureAwait` convention). This is what `small_tasks` asks for. -6. **Decide the TLS default.** Whether non-loopback connections should require verified TLS unless the operator opts out is a product decision with an upgrade note; it is the change behind three Unmet crypto criteria. +6. **Release the TLS default as a breaking version.** Verified TLS by default changes behavior for remote profiles that omit `SSL Mode`; the CHANGELOG entry carries the upgrade and rollback note (`SSL Mode=Prefer`). Choose the next 0.x minor version for it ([versioning](../CONTRIBUTING.md#automation-and-releases)) and dispatch the release yourself. 7. **Optional: sign release tags** with a signing key registered on GitHub, which would settle `version_tags_signed`. 8. **Set up access continuity.** Route 2 in [GOVERNANCE.md](../GOVERNANCE.md#what-would-change-the-status) needs no second maintainer. Give a named trusted person sealed recovery material for the GitHub, npm and NuGet accounts and written authority to use it, then record the date in GOVERNANCE.md and change `access_continuity` to Met with that evidence. Never put the material in the repository. Optionally also set a GitHub account successor. 9. **Optional: recruit a second maintainer** (route 1). This is the only path to `bus_factor`, `two_person_review`, Code-Review, Contributors and a higher Branch-Protection score. No artificial reviews or contributors are used. From 3e82b6e881b6f4df4936850d2b10a08590962a3e Mon Sep 17 00:00:00 2001 From: AG <288536783+codegiveness@users.noreply.github.com> Date: Sun, 11 Oct 2026 10:32:30 +0700 Subject: [PATCH 10/12] test: report the observed error code when a verifier error-code check fails --- tools/PostgreSqlMcp.Verify/McpClient.cs | 3 ++- tools/PostgreSqlMcp.Verify/Packages.cs | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tools/PostgreSqlMcp.Verify/McpClient.cs b/tools/PostgreSqlMcp.Verify/McpClient.cs index a7cc46a..d96ef21 100644 --- a/tools/PostgreSqlMcp.Verify/McpClient.cs +++ b/tools/PostgreSqlMcp.Verify/McpClient.cs @@ -164,7 +164,8 @@ public async Task FailsAsync(string tool, object arguments, string? co { var (error, data) = await CallAsync(tool, arguments); Check.That(error, $"Expected tool failure: {tool}."); - if (code is not null) Check.That(data["error"]?["code"].Text() == code, $"Unexpected error code from {tool}; expected {code}."); + // Codes are fixed server identifiers, never provider text, so the observed one is safe to report. + if (code is not null) Check.That(data["error"]?["code"].Text() == code, $"Unexpected error code from {tool}; expected {code}, got {data["error"]?["code"]?.Text() ?? "none"}."); if (state is not null) Check.That(data["error"]?["sql_state"].Text() == state, $"Unexpected SQLSTATE from {tool}; expected {state}."); JsonNode? target = JsonSerializer.SerializeToNode(arguments)?["database"]; if (target is not null) Check.That(JsonNode.DeepEquals(target, data["database"]), "Error response lost requested target identity."); diff --git a/tools/PostgreSqlMcp.Verify/Packages.cs b/tools/PostgreSqlMcp.Verify/Packages.cs index c5b14db..94d1d09 100644 --- a/tools/PostgreSqlMcp.Verify/Packages.cs +++ b/tools/PostgreSqlMcp.Verify/Packages.cs @@ -283,7 +283,8 @@ private static async Task VerifyCliAsync(string name, Command command, string ve ProcessResult validation = await Processes.RunAsync(command.With("--validate"), configured, expected: 1, timeout: 600); Check.That(validation.Output.Length == 0, $"{name} unavailable endpoint validation contaminated stdout."); JsonNode[] reports = Reports(validation.Error); - Check.That(reports.Any(report => report["error"]?["code"]?.Text() is "connection_error" or "timeout"), $"{name} did not diagnose the unavailable endpoint."); + Check.That(reports.Any(report => report["error"]?["code"]?.Text() is "connection_error" or "timeout"), + $"{name} did not diagnose the unavailable endpoint; observed codes: {string.Join(", ", reports.Select(report => report["error"]?["code"]?.Text() ?? "none"))}."); Check.Confidential(validation.Error, "disposable-package-secret"); } From 65caf56eab1331feb15500963cef040517d58daa Mon Sep 17 00:00:00 2001 From: AG <288536783+codegiveness@users.noreply.github.com> Date: Sun, 11 Oct 2026 10:51:14 +0700 Subject: [PATCH 11/12] fix: report a connection reset during login as connection_error Npgsql 10.0.3 can throw a raw SocketException from NpgsqlTimeout.CheckAndApply (Socket.SendTimeout on a reset socket) during Authenticate. On macOS the package verifier's accept-and-close endpoint produced it for loopback profiles now opened without TLS, and ToolReply reported internal_error. --- CHANGELOG.md | 1 + src/PostgreSqlMcp.Core/DatabaseLockCheck.cs | 3 ++- src/PostgreSqlMcp.Core/ToolReply.cs | 4 +++- 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 63faa6e..e7a3ad6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ Upgrade note: connections to non-loopback hosts now verify the server certificate unless the connection string sets `SSL Mode` explicitly (see the first entry). Everything else below is documentation, policy and tooling only. - **Breaking:** verified TLS by default. A connection string (base `POSTGRES_CONNECTION_STRING`/`--connection-string` or any targets JSON/file profile) that does not set `SSL Mode` previously got Npgsql's `Prefer`, which uses TLS without verifying the server and silently falls back to plaintext. It now gets `SSL Mode=VerifyFull` when its host is not loopback (certificate chain and host name are verified before any credential is sent, with no plaintext or unverified fallback) and `SSL Mode=Disable` when every host is `localhost`, `127.0.0.0/8`, `::1` or a Unix-domain socket (no TLS is used, so no TLS without verification; a `Root Certificate` on such a profile selects `VerifyFull`). An explicit `SSL Mode`, including `Prefer`, `Require` and `Disable`, is the operator's opt-out and is honored unchanged; the existing startup/`--validate` `[warning]` still flags non-loopback profiles without `VerifyCA`/`VerifyFull`. New fixed errors, on stderr for `--validate` and as structured tool errors, name only the profile and the fix: `tls_verification_failed` (untrusted CA or host name not in the certificate: add `Root Certificate=`, or opt out with `SSL Mode=Require`) and `tls_unavailable` (the server offers no TLS: enable it, or opt out with `SSL Mode=Disable`). **Upgrade guidance:** servers with a publicly trusted certificate need no change; a private CA needs `Root Certificate=`; a non-TLS remote server, including a host PostgreSQL reached as `host.docker.internal`, needs `SSL Mode=Disable`; a loopback or forwarded server that requires TLS (`hostssl` rules) needs an explicit `SSL Mode`. To restore the old behavior for a profile add `SSL Mode=Prefer` explicitly. Run `--validate` before reconnecting your client. `--help`, README, SECURITY and the assurance case are updated. Verifier: a disposable TLS container with a throwaway CA proves that an unset mode refuses an unverifiable non-loopback server before the login is sent (the server log shows no user), succeeds with `Root Certificate`, honors `Require` and `Disable` with the warning, rejects a host name missing from a trusted certificate, and uses plaintext on loopback. +- A connection the server resets during login now reports `connection_error` instead of `internal_error` ("Unexpected server failure"), and the startup database-lock check reports it as a connection finding instead of failing. Npgsql 10.0.3 can let a raw `SocketException` escape while it applies a socket timeout to a connection that was already reset; the package verifier hit this on macOS when opening a loopback profile without TLS. - Add [GOVERNANCE.md](GOVERNANCE.md) (decision-making, roles and what happens if the sole maintainer is unavailable; it states that `access_continuity` is not met), [docs/architecture.md](docs/architecture.md), [docs/roadmap.md](docs/roadmap.md) and [docs/assurance-case.md](docs/assurance-case.md) (requirements, threat model, trust boundaries, secure-design principles, common weaknesses and residual risks; the review record is pending the maintainer). - CONTRIBUTING: add coding standards, code-review standards and small-task guidance. Add an `.editorconfig` and a CI step (`dotnet format style --verify-no-changes --severity warn`) that enforces the C# style rules. Source files gain SPDX copyright and license headers, and unused `using` directives are removed. - SECURITY: document how vulnerability reports are handled. diff --git a/src/PostgreSqlMcp.Core/DatabaseLockCheck.cs b/src/PostgreSqlMcp.Core/DatabaseLockCheck.cs index 938779b..293030c 100644 --- a/src/PostgreSqlMcp.Core/DatabaseLockCheck.cs +++ b/src/PostgreSqlMcp.Core/DatabaseLockCheck.cs @@ -99,7 +99,8 @@ public static async Task> VerifyAsync(DatabaseRegistry reg } catch (ToolException ex) { findings.Add($"target {target}: lock check failed ({ex.Code}): {ex.Message}"); } catch (PostgresException ex) { findings.Add($"target {target}: lock check failed (SQLSTATE {ex.SqlState})."); } - catch (NpgsqlException) { findings.Add($"target {target}: lock check could not connect or authenticate."); } + // A raw SocketException is the same transport failure (see ToolReply.Run). + catch (Exception ex) when (ex is NpgsqlException or System.Net.Sockets.SocketException) { findings.Add($"target {target}: lock check could not connect or authenticate."); } catch (TimeoutException) { findings.Add($"target {target}: lock check timed out."); } } return findings; diff --git a/src/PostgreSqlMcp.Core/ToolReply.cs b/src/PostgreSqlMcp.Core/ToolReply.cs index e94321e..a3b1f6f 100644 --- a/src/PostgreSqlMcp.Core/ToolReply.cs +++ b/src/PostgreSqlMcp.Core/ToolReply.cs @@ -39,7 +39,9 @@ public static async Task Run(string database, Func> catch (OperationCanceledException) { throw; } catch (NpgsqlException ex) when (ex.InnerException is TimeoutException) { return Error(database, "timeout", "Connection or command timed out; no fallback was used."); } - catch (NpgsqlException) + // Npgsql 10.0.3 lets a raw SocketException escape when it applies a socket timeout to a connection the peer already + // reset (observed on macOS while opening without TLS); it is the same transport failure, not a server bug. + catch (Exception ex) when (ex is NpgsqlException or System.Net.Sockets.SocketException) { return Error(database, "connection_error", "Cannot connect to or communicate with the requested target. Check configured credentials, network and database. No fallback was used."); } catch (TimeoutException) { return Error(database, "timeout", "Operation timed out; no fallback was used."); } catch (Exception) From 0ce8829a1accbf4eee01a2181071c2bc3e5c5554 Mon Sep 17 00:00:00 2001 From: AG <288536783+codegiveness@users.noreply.github.com> Date: Sun, 11 Oct 2026 11:52:42 +0700 Subject: [PATCH 12/12] docs: mark governance, roadmap and assurance case as unconfirmed drafts and record owner actions GOVERNANCE.md, docs/roadmap.md and docs/assurance-case.md now open with an explicit unconfirmed-maintainer-draft notice. The owner checklist records the verified provenance bundles attached to v0.3.1-v0.4.0, that no open issue existed to label, and the secret-scanning settings read without change. TLS answers in .bestpractices.json and the architecture diagram describe the new verified default instead of Npgsql's Prefer. --- .bestpractices.json | 14 +++++++------- GOVERNANCE.md | 2 ++ docs/architecture.md | 2 +- docs/assurance-case.md | 2 +- docs/roadmap.md | 4 +++- docs/security-posture.md | 26 ++++++++++++-------------- 6 files changed, 26 insertions(+), 24 deletions(-) diff --git a/.bestpractices.json b/.bestpractices.json index a6f7caa..5cf7b81 100644 --- a/.bestpractices.json +++ b/.bestpractices.json @@ -96,13 +96,13 @@ "delivery_unsigned_status": "Met", "delivery_unsigned_justification": "Public-source evidence: the project never retrieves a cryptographic hash over HTTP. CI, build and release tooling download third-party binaries only over HTTPS and compare them with SHA-256 or SHA-512 digests committed in the repository: https://github.com/codegiveness/postgresql-sharp-mcp/blob/fbbe5aa0c2abea5d48d58fa282d84204921c8e19/.github/workflows/secrets.yml#L23-L35 and https://github.com/codegiveness/postgresql-sharp-mcp/blob/fbbe5aa0c2abea5d48d58fa282d84204921c8e19/.github/workflows/release.yml#L193-L202 and https://github.com/codegiveness/postgresql-sharp-mcp/blob/fbbe5aa0c2abea5d48d58fa282d84204921c8e19/tools/PostgreSqlMcp.Fuzz/FuzzCampaign.cs#L8-L9 and https://github.com/codegiveness/postgresql-sharp-mcp/blob/fbbe5aa0c2abea5d48d58fa282d84204921c8e19/tools/PostgreSqlMcp.Build/Program.cs#L16-L17 . Outside bundled third-party license texts, no http:// URL appears in the repository at this revision. Release SHA256SUMS manifests are delivered with the assets over HTTPS from GitHub Releases and covered by GitHub build attestations: https://github.com/codegiveness/postgresql-sharp-mcp/blob/fbbe5aa0c2abea5d48d58fa282d84204921c8e19/docs/security-posture.md#supply-chain-evidence . npm and NuGet also deliver packages over HTTPS.", "crypto_keylength_status": "Met", - "crypto_keylength_justification": "Public-source evidence: the application selects no key sizes and contains no cryptographic APIs: https://github.com/codegiveness/postgresql-sharp-mcp/tree/fbbe5aa0c2abea5d48d58fa282d84204921c8e19/src . Its security mechanisms are delegated. Npgsql 10.0.3 SCRAM-SHA-256 authentication uses SHA-256, HMAC-SHA-256 and PBKDF2-SHA-256 with 256-bit outputs: https://github.com/npgsql/npgsql/blob/d3768398c17877b3a916c3c4d87e8e11698991fc/src/Npgsql/Internal/NpgsqlConnector.Auth.cs#L150-L284 . TLS uses .NET SslStream with EnabledSslProtocols=None and no cipher override, so the operating system's TLS policy chooses protocol versions, cipher suites and key-exchange groups: https://github.com/npgsql/npgsql/blob/d3768398c17877b3a916c3c4d87e8e11698991fc/src/Npgsql/Internal/NpgsqlConnector.cs#L1229-L1239 and https://learn.microsoft.com/dotnet/core/extensions/sslstream-best-practices . Smaller key lengths can be completely disabled through that OS policy (for example the OpenSSL security level on Linux or SChannel settings on Windows); the application has no separate key-length option. Negotiated key sizes also depend on the operator's PostgreSQL server certificate and TLS settings, and TLS is used only when the operator's SSL Mode and server enable it (Npgsql defaults to Prefer): https://github.com/codegiveness/postgresql-sharp-mcp/blob/fbbe5aa0c2abea5d48d58fa282d84204921c8e19/SECURITY.md#connection-and-resource-settings . This is not a guarantee for every platform or server.", + "crypto_keylength_justification": "Public-source evidence: the application selects no key sizes and contains no cryptographic APIs: https://github.com/codegiveness/postgresql-sharp-mcp/tree/fbbe5aa0c2abea5d48d58fa282d84204921c8e19/src . Its security mechanisms are delegated. Npgsql 10.0.3 SCRAM-SHA-256 authentication uses SHA-256, HMAC-SHA-256 and PBKDF2-SHA-256 with 256-bit outputs: https://github.com/npgsql/npgsql/blob/d3768398c17877b3a916c3c4d87e8e11698991fc/src/Npgsql/Internal/NpgsqlConnector.Auth.cs#L150-L284 . TLS uses .NET SslStream with EnabledSslProtocols=None and no cipher override, so the operating system's TLS policy chooses protocol versions, cipher suites and key-exchange groups: https://github.com/npgsql/npgsql/blob/d3768398c17877b3a916c3c4d87e8e11698991fc/src/Npgsql/Internal/NpgsqlConnector.cs#L1229-L1239 and https://learn.microsoft.com/dotnet/core/extensions/sslstream-best-practices . Smaller key lengths can be completely disabled through that OS policy (for example the OpenSSL security level on Linux or SChannel settings on Windows); the application has no separate key-length option. Negotiated key sizes also depend on the operator's PostgreSQL server certificate and TLS settings. A profile that omits SSL Mode requires verified TLS for a non-loopback host and uses no TLS for loopback; an explicit SSL Mode is kept: https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/src/PostgreSqlMcp.Core/TlsPolicy.cs . This is not a guarantee for every platform or server.", "crypto_working_status": "Met", - "crypto_working_justification": "Public-source evidence: the application implements no algorithm or cipher mode and contains no cryptographic APIs: https://github.com/codegiveness/postgresql-sharp-mcp/tree/fbbe5aa0c2abea5d48d58fa282d84204921c8e19/src . By default Npgsql 10.0.3 authenticates with SCRAM-SHA-256, trying SCRAM-SHA-256-PLUS channel binding first over TLS, whenever the server requests SASL, as PostgreSQL 14 and later do by default: https://github.com/npgsql/npgsql/blob/d3768398c17877b3a916c3c4d87e8e11698991fc/src/Npgsql/Internal/NpgsqlConnector.Auth.cs#L50-L58 . TLS uses .NET SslStream with OS-chosen protocols and cipher suites, which .NET documents as letting the OS block versions it no longer considers secure: https://github.com/npgsql/npgsql/blob/d3768398c17877b3a916c3c4d87e8e11698991fc/src/Npgsql/Internal/NpgsqlConnector.cs#L1229-L1239 and https://learn.microsoft.com/dotnet/core/extensions/sslstream-best-practices . Npgsql still performs PostgreSQL's legacy MD5 challenge-response if an operator's server is configured to request it; operators can refuse it with the Require Auth connection-string setting (for example ScramSHA256): https://github.com/npgsql/npgsql/blob/d3768398c17877b3a916c3c4d87e8e11698991fc/src/Npgsql/NpgsqlConnectionStringBuilder.cs#L724-L731 . The role-hardening guide configures hostssl with scram-sha-256: https://github.com/codegiveness/postgresql-sharp-mcp/blob/fbbe5aa0c2abea5d48d58fa282d84204921c8e19/SECURITY.md#hardening-the-role . TLS use and certificate validation remain operator-controlled: https://github.com/codegiveness/postgresql-sharp-mcp/blob/fbbe5aa0c2abea5d48d58fa282d84204921c8e19/SECURITY.md#connection-and-resource-settings", + "crypto_working_justification": "Public-source evidence: the application implements no algorithm or cipher mode and contains no cryptographic APIs: https://github.com/codegiveness/postgresql-sharp-mcp/tree/fbbe5aa0c2abea5d48d58fa282d84204921c8e19/src . By default Npgsql 10.0.3 authenticates with SCRAM-SHA-256, trying SCRAM-SHA-256-PLUS channel binding first over TLS, whenever the server requests SASL, as PostgreSQL 14 and later do by default: https://github.com/npgsql/npgsql/blob/d3768398c17877b3a916c3c4d87e8e11698991fc/src/Npgsql/Internal/NpgsqlConnector.Auth.cs#L50-L58 . TLS uses .NET SslStream with OS-chosen protocols and cipher suites, which .NET documents as letting the OS block versions it no longer considers secure: https://github.com/npgsql/npgsql/blob/d3768398c17877b3a916c3c4d87e8e11698991fc/src/Npgsql/Internal/NpgsqlConnector.cs#L1229-L1239 and https://learn.microsoft.com/dotnet/core/extensions/sslstream-best-practices . Npgsql still performs PostgreSQL's legacy MD5 challenge-response if an operator's server is configured to request it; operators can refuse it with the Require Auth connection-string setting (for example ScramSHA256): https://github.com/npgsql/npgsql/blob/d3768398c17877b3a916c3c4d87e8e11698991fc/src/Npgsql/NpgsqlConnectionStringBuilder.cs#L724-L731 . The role-hardening guide configures hostssl with scram-sha-256: https://github.com/codegiveness/postgresql-sharp-mcp/blob/fbbe5aa0c2abea5d48d58fa282d84204921c8e19/SECURITY.md#hardening-the-role . A profile that omits SSL Mode uses verified TLS (VerifyFull) for a non-loopback host and no TLS for loopback; an explicit SSL Mode is the operator's choice and is kept: https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/src/PostgreSqlMcp.Core/TlsPolicy.cs", "crypto_weaknesses_status": "Met", "crypto_weaknesses_justification": "Public-source evidence: the delegated default mechanisms do not depend on SHA-1 or on CBC modes chosen by this project. SCRAM-SHA-256 uses only SHA-256-family primitives, and SCRAM channel binding hashes SHA-1- or MD5-signed server certificates with SHA-256: https://github.com/npgsql/npgsql/blob/d3768398c17877b3a916c3c4d87e8e11698991fc/src/Npgsql/Internal/NpgsqlConnector.Auth.cs#L150-L284 . TLS algorithms come from the operating system's default policy through SslStream rather than an application default: https://github.com/npgsql/npgsql/blob/d3768398c17877b3a916c3c4d87e8e11698991fc/src/Npgsql/Internal/NpgsqlConnector.cs#L1229-L1239 and https://learn.microsoft.com/dotnet/core/extensions/sslstream-best-practices . TLS 1.3, negotiated when the OS and the operator's PostgreSQL server support it, offers only AEAD cipher suites. Whether a legacy TLS 1.2 CBC or SHA-1 suite could be negotiated depends on that OS policy and the operator's server configuration: https://github.com/codegiveness/postgresql-sharp-mcp/blob/fbbe5aa0c2abea5d48d58fa282d84204921c8e19/SECURITY.md#connection-and-resource-settings . Legacy MD5 authentication is covered under crypto_working.", "crypto_pfs_status": "Met", - "crypto_pfs_justification": "Public-source evidence: the only key-agreement protocol is TLS, implemented by .NET SslStream and the operating system's TLS provider with OS-selected protocols and suites: https://github.com/npgsql/npgsql/blob/d3768398c17877b3a916c3c4d87e8e11698991fc/src/Npgsql/Internal/NpgsqlConnector.cs#L1229-L1239 . Full TLS 1.3 handshakes, used when the OS and the operator's PostgreSQL server support TLS 1.3, always use ephemeral (EC)DHE key exchange and so provide forward secrecy; with TLS 1.2, forward secrecy depends on the OS policy and the server preferring ECDHE or DHE suites. TLS use, certificate validation and server TLS settings are operator-controlled, and the server does not force TLS: https://github.com/codegiveness/postgresql-sharp-mcp/blob/fbbe5aa0c2abea5d48d58fa282d84204921c8e19/SECURITY.md#connection-and-resource-settings . SCRAM authentication is not a key-agreement protocol. This is not a guarantee for every deployment.", + "crypto_pfs_justification": "Public-source evidence: the only key-agreement protocol is TLS, implemented by .NET SslStream and the operating system's TLS provider with OS-selected protocols and suites: https://github.com/npgsql/npgsql/blob/d3768398c17877b3a916c3c4d87e8e11698991fc/src/Npgsql/Internal/NpgsqlConnector.cs#L1229-L1239 . Full TLS 1.3 handshakes, used when the OS and the operator's PostgreSQL server support TLS 1.3, always use ephemeral (EC)DHE key exchange and so provide forward secrecy; with TLS 1.2, forward secrecy depends on the OS policy and the server preferring ECDHE or DHE suites. A profile that omits SSL Mode requires verified TLS for a non-loopback host and uses no TLS for loopback; an explicit SSL Mode, including one that disables TLS, is the operator's choice and is kept: https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/src/PostgreSqlMcp.Core/TlsPolicy.cs . The server's TLS settings are the operator's. SCRAM authentication is not a key-agreement protocol. This is not a guarantee for every deployment.", "crypto_random_status": "Met", "crypto_random_justification": "Public-source evidence: the application generates no cryptographic keys or nonces itself and contains no cryptographic or random-number APIs: https://github.com/codegiveness/postgresql-sharp-mcp/tree/fbbe5aa0c2abea5d48d58fa282d84204921c8e19/src . Npgsql 10.0.3 generates the SCRAM client nonce with System.Security.Cryptography.RandomNumberGenerator, a cryptographically secure generator: https://github.com/npgsql/npgsql/blob/d3768398c17877b3a916c3c4d87e8e11698991fc/src/Npgsql/Internal/NpgsqlConnector.Auth.cs#L194-L201 . TLS session keys and nonces are generated by the operating system's TLS provider used by .NET SslStream: https://github.com/npgsql/npgsql/blob/d3768398c17877b3a916c3c4d87e8e11698991fc/src/Npgsql/Internal/NpgsqlConnector.cs#L1229-L1239 . Neither the application nor Npgsql uses System.Random for these values.", "test_most_status": "Met", @@ -218,7 +218,7 @@ "crypto_verification_private_status": "Met", "crypto_verification_private_justification": "Met. Under the default (VerifyFull for non-loopback hosts) Npgsql verifies the server certificate during the TLS handshake, before the startup packet (user name) or any password is sent. The verifier's TLS scenario asserts that after a failed verification the PostgreSQL server log contains no login attempt for the configured role. Operators who explicitly set Prefer, Allow, Require or Disable opt out; that opt-out is documented and warned about for remote hosts: https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/SECURITY.md#connection-and-resource-settings . These files are changed by the pull request that proposes these answers; the URLs show the described state once that change is merged to main.", "signed_releases_status": "Met", - "signed_releases_justification": "Releases from v0.2.0 carry GitHub build attestations (Sigstore keyless signatures bound to the release.yml workflow identity and the exact source commit) for every archive, package, the SBOM and SHA256SUMS; since v0.5.0 the signed bundle is also published as the provenance.sigstore.json release asset (gh release view v0.5.0 listed it on 2026-10-11). No long-lived private signing key exists on the distribution sites. The documented verification process, which checks repository, signer workflow and subject digest with gh attestation verify, is in https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/docs/security-posture.md#supply-chain-evidence (the release verification commands near the end of that section). v0.1.0 predates attestations.", + "signed_releases_justification": "Releases from v0.2.0 carry GitHub build attestations (Sigstore keyless signatures bound to the release.yml workflow identity and the exact source commit) for every archive, package, the SBOM and SHA256SUMS; since v0.5.0 the signed bundle is also published as the provenance.sigstore.json release asset (gh release view v0.5.0 listed it on 2026-10-11), and on 2026-10-11 the existing bundles of v0.3.1, v0.3.2, v0.3.3 and v0.4.0 were attached the same way after each verified with gh attestation verify against every asset of its release. No long-lived private signing key exists on the distribution sites. The documented verification process, which checks repository, signer workflow and subject digest with gh attestation verify, is in https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/docs/security-posture.md#supply-chain-evidence (the release verification commands near the end of that section). v0.1.0 predates attestations.", "version_tags_signed_status": "Unmet", "version_tags_signed_justification": "Release tags are annotated but not cryptographically signed: GitHub's tag API reported verified=false (reason unsigned) for v0.1.0, v0.2.0, v0.3.1, v0.3.2, v0.4.0 and v0.5.0 on 2026-10-11, and v0.3.3 is a lightweight tag. Signed tags (git tag -s with a signing key registered on GitHub) would change this.", "input_validation_status": "Met", @@ -226,7 +226,7 @@ "hardening_status": "Met", "hardening_justification": "The software is managed, memory-safe C# with no unsafe code or native interop in src; nullable reference types and warnings-as-errors remove classes of undefined behavior (https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/Directory.Build.props ); the server bounds results, statements, concurrency and pools and forces safe Npgsql settings so defects are less likely to become vulnerabilities (https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/docs/assurance-case.md#4-argument-that-the-requirements-are-met ). The project has no HTTP interface, so CSP and similar headers do not apply. Least privilege (non-root container, narrow tokens) is counted separately and not claimed as hardening. This file is added by the pull request that proposes these answers; its URL resolves once that change is merged to main.", "assurance_case_status": "Met", - "assurance_case_justification": "docs/assurance-case.md contains the threat model, the trust boundaries, an argument that secure design principles were applied, an argument that common weaknesses are countered, and the residual risks and gaps: https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/docs/assurance-case.md . It is a self-assessment drafted by an AI agent for the maintainer and says so; the maintainer's review is recorded in the document. This file is added by the pull request that proposes these answers; its URL resolves once that change is merged to main.", + "assurance_case_justification": "docs/assurance-case.md contains the threat model, the trust boundaries, an argument that secure design principles were applied, an argument that common weaknesses are countered, and the residual risks and gaps: https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/docs/assurance-case.md . It is a self-assessment drafted by an AI agent for the maintainer and says so; its review record shows the maintainer's review as pending until the maintainer records it. This file is added by the pull request that proposes these answers; its URL resolves once that change is merged to main.", "contributors_unassociated_status": "Unmet", "contributors_unassociated_justification": "The project has one human contributor; no second unassociated significant contributor exists (GitHub contributors API, 2026-10-11).", "copyright_per_file_status": "Met", @@ -234,7 +234,7 @@ "license_per_file_status": "Met", "license_per_file_justification": "Every tracked .cs source file begins with SPDX-License-Identifier: MIT (for example https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/src/PostgreSqlMcp.Core/SqlGuard.cs ); the license text is https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/LICENSE . Added to all 39 .cs files by the pull request that proposes these answers. Non-source data files such as fuzz corpus seeds are not annotated, because changing their bytes would change the corpus.", "small_tasks_status": "Unmet", - "small_tasks_justification": "The good first issue and help wanted labels exist and CONTRIBUTING explains them (https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/CONTRIBUTING.md#small-tasks-for-new-contributors ), but no open issue carries either label on 2026-10-11. Labeling real small tasks would change this.", + "small_tasks_justification": "The good first issue and help wanted labels exist and CONTRIBUTING explains them (https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/CONTRIBUTING.md#small-tasks-for-new-contributors ), but on 2026-10-11 the repository had no open issues at all (0 open, 133 closed), so none carries either label. Labeling real small tasks when they arise would change this.", "code_review_standards_status": "Met", "code_review_standards_justification": "CONTRIBUTING documents how review is conducted, what must be checked and what makes a change acceptable: https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/CONTRIBUTING.md#code-review-standards , with the self-review procedure in https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/docs/git-workflow.md#6-review . This file is added by the pull request that proposes these answers; its URL resolves once that change is merged to main.", "two_person_review_status": "Unmet", @@ -247,5 +247,5 @@ "test_branch_coverage80_justification": "Met. Total branch coverage of the shipped assemblies under the integration scenarios was 84.84% (907/1069) on 2026-10-11, measured with the FLOSS coverlet console tool by the verifier's coverage command, which CI runs on every pull request (its gate is on line coverage; branch coverage is reported alongside): https://github.com/codegiveness/postgresql-sharp-mcp/blob/main/CONTRIBUTING.md#measure-test-coverage . These files are changed by the pull request that proposes these answers; the URLs show the described state once that change is merged to main.", "hardened_site_status": "Met", "hardened_site_justification": "The repository, issue tracker and release downloads are served by github.com, which sent Strict-Transport-Security, Content-Security-Policy, X-Content-Type-Options: nosniff and X-Frame-Options: deny in responses to curl -I on 2026-10-11 for https://github.com/codegiveness/postgresql-sharp-mcp and the v0.5.0 release asset URL (the asset itself is then served from a GitHub-controlled redirect target that was not separately checked). The package registries are third-party sites: npmjs.com returned HSTS, a CSP, nosniff and X-Frame-Options; the nuget.org package page returned HSTS, nosniff and X-Frame-Options but its CSP was report-only.", - "general_comments": "Evidence-backed project metadata for https://www.bestpractices.dev/en/projects/15155 . Proposed answers require review and saving on the badge website. Silver and gold proposals were added on 2026-10-11 and cite files that exist only after the pull request that adds them is merged. Not inferred and therefore not proposed: require_2FA and secure_2FA (the GitHub account's 2FA method is not observable from the repository) and security_review (a human review of docs/assurance-case.md must be recorded in that document first). Also not inferred: developer knowledge beyond the owner's attestation; response timeliness for reports not yet received (0 issues, security advisories and Dependabot alerts and 0 open CodeQL alerts as checked through the GitHub API on 2026-10-10 and again on 2026-10-11); negotiated TLS key lengths, algorithms or forward secrecy beyond Npgsql, .NET and OS defaults and the operator's SSL Mode and PostgreSQL server settings; absence of vulnerabilities or credentials beyond the cited point-in-time scans; branch and statement coverage; and pre-release static analysis for v0.1.0, which predates the CodeQL workflow." + "general_comments": "Evidence-backed project metadata for https://www.bestpractices.dev/en/projects/15155 . Proposed answers require review and saving on the badge website. Silver and gold proposals were added on 2026-10-11 and cite files that exist only after the pull request that adds them is merged. Not inferred and therefore not proposed: require_2FA and secure_2FA (the GitHub account's 2FA method is not observable from the repository) and security_review (a human review of docs/assurance-case.md must be recorded in that document first). Also not inferred: developer knowledge beyond the owner's attestation; response timeliness for reports not yet received (0 issues, security advisories and Dependabot alerts and 0 open CodeQL alerts as checked through the GitHub API on 2026-10-10 and again on 2026-10-11); negotiated TLS key lengths, algorithms or forward secrecy beyond Npgsql, .NET and OS defaults and the operator's SSL Mode and PostgreSQL server settings; absence of vulnerabilities or credentials beyond the cited point-in-time scans; and pre-release static analysis for v0.1.0, which predates the CodeQL workflow." } diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 968ad85..5eb4ccd 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -1,5 +1,7 @@ # Governance +**Status: unconfirmed maintainer draft.** An AI coding agent wrote this document on 2026-10-11 for the maintainer to review. The maintainer has not confirmed it yet. Until this notice is removed, it is a proposed description of the project's governance, not an adopted policy. The repository settings it cites are in force either way. + This document says how decisions are made in postgresql-sharp-mcp, who holds which role, and what happens if the maintainer is unavailable. It describes the project as it is on 2026-10-11, not as it might become. The project has one maintainer; where that limits what it can promise, the text says so. ## Governance model diff --git a/docs/architecture.md b/docs/architecture.md index 5960355..3e6708d 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -11,7 +11,7 @@ Checked against the source of release 0.5.0 (commit `64c55c7`) on 2026-10-11. | JSON-RPC over stdin/stdout v postgresql-sharp-mcp (one process) -- diagnostics on stderr only - | Npgsql (PostgreSQL wire protocol, TLS as configured by the operator) + | Npgsql (PostgreSQL wire protocol; TLS verified by default, explicit SSL Mode kept) v PostgreSQL server(s): the authorization boundary ``` diff --git a/docs/assurance-case.md b/docs/assurance-case.md index d29e6ef..0945d67 100644 --- a/docs/assurance-case.md +++ b/docs/assurance-case.md @@ -2,7 +2,7 @@ This document argues why postgresql-sharp-mcp meets its security requirements, and says plainly where it does not. It contains the security requirements (what you can and cannot expect), the threat model, the trust boundaries, an argument that secure design principles were applied, an argument that common implementation weaknesses are countered, and the residual risks. Each claim points to code, documentation or a check you can rerun. -**Status.** Drafted on 2026-10-11 against release 0.5.0 (commit `64c55c7`) by an AI coding agent working for the maintainer, from the source and the project's documents. It is a self-assessment, not an independent audit or certification. The [review record](#review-record) shows whether the maintainer has reviewed it. Report errors through the process in [SECURITY.md](../SECURITY.md#reporting-a-vulnerability). Component descriptions are in [docs/architecture.md](architecture.md). +**Status: unconfirmed maintainer draft.** Drafted on 2026-10-11 against release 0.5.0 (commit `64c55c7`) by an AI coding agent working for the maintainer, from the source and the project's documents. The maintainer has not reviewed it yet; the [review record](#review-record) shows when that happens. It is a self-assessment, not an independent audit or certification. Report errors through the process in [SECURITY.md](../SECURITY.md#reporting-a-vulnerability). Component descriptions are in [docs/architecture.md](architecture.md). ## 1. Security requirements diff --git a/docs/roadmap.md b/docs/roadmap.md index c3e2fd9..9b1e003 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,5 +1,7 @@ # Roadmap +**Status: unconfirmed maintainer draft.** An AI coding agent wrote this roadmap on 2026-10-11 from the gaps the project has documented, for the maintainer to review. The maintainer has not confirmed it yet. Until this notice is removed, treat it as a proposal. + This is the direction for the twelve months from 2026-10-11 to 2027-10-11, so that users and contributors can see what the project intends to do and not do. It is not a promise: the project has one maintainer ([governance](../GOVERNANCE.md)), the software is a 0.x release series, and dates depend on the maintainer's time. Items that need someone other than the maintainer are marked as such. The list is derived from gaps the project has documented itself in [docs/security-posture.md](security-posture.md), [docs/assurance-case.md](assurance-case.md) and the [OpenSSF Best Practices answers](security-posture.md#openssf-best-practices-enrollment). ## What the project intends to do @@ -15,7 +17,7 @@ This is the direction for the twelve months from 2026-10-11 to 2027-10-11, so th - **Statement coverage above 90%.** The integration scenarios reach 89.41% of statements and 84.84% of branches (measured 2026-10-11; CI fails below 80% lines, see [Measure test coverage](../CONTRIBUTING.md#measure-test-coverage)). The least-covered file is `src/PostgreSqlMcp/Program.cs`: a scenario for `--help`, `--version` and an invalid `--log-level` would close most of the gap to 90%. - **Independently reproduced release.** Every release asset except the per-run provenance bundle is now byte-identical across rebuilds of the same source. The next release is the first whose `.nupkg` and SBOM can be checked against an independent rebuild. -- **Release provenance visibility.** Releases from 0.5.0 carry `provenance.sigstore.json`, which is how Scorecard's Signed-Releases check sees the signatures. Each new release extends that coverage. +- **Release provenance visibility.** Releases from v0.3.1 carry `provenance.sigstore.json` (0.5.0 at publication; v0.3.1 to v0.4.0 since 2026-10-11), which is how Scorecard's Signed-Releases check sees the signatures. Each new release keeps that coverage. - **Assurance case upkeep.** Review and update the assurance case whenever the trust boundary, a default or a release path changes, and at least once a year. ### Sustain the project diff --git a/docs/security-posture.md b/docs/security-posture.md index a865b4b..277c9e1 100644 --- a/docs/security-posture.md +++ b/docs/security-posture.md @@ -70,7 +70,7 @@ The [report for `64c55c7`](https://api.securityscorecards.dev/projects/github.co | Contributors | 0 → 0 | 0 contributing organizations | Contributors with at least five commits each from at least three organizations. | | Maintained | 0 → 0 | Repository created within the last 90 days | Time. Age-eligible from 2026-12-31 02:18:32 UTC, then sustained commit and issue activity (10 needs about one per week) gives 10; artificial activity is not used. | | CII-Best-Practices | 5 → 5 | Badge detected: Passing | Silver scores 7 and Gold 10. See [what stays unmet](#what-stays-unmet-and-what-would-change-it). | -| Signed-Releases | 0 → 1 | Only v0.5.0 has a signed artifact (`provenance.sigstore.json`); v0.3.1 to v0.4.0 have none | Release 0.5.0 is observed to carry `provenance.sigstore.json` (release assets read on 2026-10-11). Each of the four older releases in Scorecard's window that receives its existing bundle adds 8/5 to the score, and each new release replaces the oldest; five signed releases score 8. 10 needs `.intoto.jsonl` provenance, which conflicts with the full-SHA pin policy ([above](#published-scorecard-findings)). | +| Signed-Releases | 0 → 1 | Only v0.5.0 has a signed artifact (`provenance.sigstore.json`); v0.3.1 to v0.4.0 have none | Release 0.5.0 is observed to carry `provenance.sigstore.json` (release assets read on 2026-10-11). Later on 2026-10-11 the four older releases in Scorecard's window received their existing, verified bundles ([owner action 4](#owner-action-checklist)), so all five releases in the window now carry one; each signed release counts 8, so the next report should score 8 [INFERENCE: not yet observed in a Scorecard report]. Each new release replaces the oldest. 10 needs `.intoto.jsonl` provenance, which conflicts with the full-SHA pin policy ([above](#published-scorecard-findings)). | | SAST | 7 → 9 | 25 of the last 30 commits are checked | Time and normal merges: five older commits leave the sample as new PRs merge. | | Packaging | 10 → −1 | No GitHub/GitLab publishing workflow detected | Not a loss: −1 means the check is inconclusive and is excluded from the aggregate. Scorecard does not say why detection changed; the change coincides with 0.5.0 moving NuGet publication from an inline `dotnet nuget push` to the compiled release tool [INFERENCE]. The workflow is not reshaped to satisfy the heuristic. | @@ -96,7 +96,7 @@ From release 0.2.0, the release workflow generates: 1. Five self-contained platform archives, npm tarballs (the main package and five platform packages, each platform package holding one self-contained runtime) and a NuGet tool package. 2. An application CycloneDX JSON SBOM generated by the pinned .NET CycloneDX tool from the resolved NuGet graph, excluding dependencies classified as development-only. Some build-time transitive packages may remain; this is not a complete operating-system/container/native-library inventory. 3. A `SHA256SUMS` manifest covering the five archives, npm/NuGet packages and SBOM. Checksums detect mismatched bytes; an unauthenticated checksum download is not proof of publisher identity. -4. Requested GitHub build attestations for the archives, npm/NuGet package files, SBOM and checksum manifest. Verify the subject digest and repository/workflow identity; do not infer attestation success from workflow configuration. The attested NuGet subject is the GitHub asset, not a claim that registry signing preserves those bytes. Releases dispatched after the provenance-bundle change reached `main` also carry that attestation's Sigstore bundle as the `provenance.sigstore.json` GitHub Release asset; the release tool refuses to publish unless the bundle is one SLSA v1 provenance statement whose subjects are exactly the uploaded assets and their SHA-256 digests. Releases up to v0.4.0 have attestations in GitHub's store but no bundle asset. Release 0.5.0, published 2026-10-11 01:12:33 UTC, lists `provenance.sigstore.json` among its release assets (read through `gh release view v0.5.0` on 2026-10-11). +4. Requested GitHub build attestations for the archives, npm/NuGet package files, SBOM and checksum manifest. Verify the subject digest and repository/workflow identity; do not infer attestation success from workflow configuration. The attested NuGet subject is the GitHub asset, not a claim that registry signing preserves those bytes. Releases dispatched after the provenance-bundle change reached `main` also carry that attestation's Sigstore bundle as the `provenance.sigstore.json` GitHub Release asset; the release tool refuses to publish unless the bundle is one SLSA v1 provenance statement whose subjects are exactly the uploaded assets and their SHA-256 digests. Release 0.5.0, published 2026-10-11 01:12:33 UTC, lists `provenance.sigstore.json` among its release assets (read through `gh release view v0.5.0` on 2026-10-11). Releases v0.3.1 to v0.4.0 were published with attestations only in GitHub's store; on 2026-10-11 each received its existing bundle as `provenance.sigstore.json` after the bundle verified against every one of its assets ([owner action 4](#owner-action-checklist)). v0.2.0 and earlier have no bundle asset. 5. Requested npm provenance for a successful registry publication. NuGet trusted publishing exchanges GitHub OIDC for a short-lived API key; this authenticates publication and is not equivalent to npm provenance. ### Observed 0.3.3 release @@ -207,7 +207,7 @@ Silver still fails on one MUST criterion, `access_continuity`, so `achieve_silve | `two_person_review`; Scorecard Code-Review | Gold MUST; check | Every human change is authored by the maintainer and GitHub blocks self-approval; Scorecard found 0 of 27 changesets approved | A different human reviewing and approving at least half of proposed modifications, or the maintainer approving outside contributors' PRs. Bot or agent approvals do not count | | Scorecard Branch-Protection above 4 | Check | Required approvers, code-owner review and last-push approval are off because they would block a sole maintainer | Enable them only once a second reviewer exists | | Scorecard Maintained | Check | Repository younger than 90 days | Time: age-eligible 2026-12-31, then normal activity | -| Scorecard Signed-Releases above 8 | Check | The GitHub bundle is not `.intoto.jsonl` | A tag-referenced SLSA generator, rejected by the full-SHA pin policy. Reaching 8 needs bundles on the five newest releases | +| Scorecard Signed-Releases above 8 | Check | The GitHub bundle is not `.intoto.jsonl` | A tag-referenced SLSA generator, rejected by the full-SHA pin policy. 8 needs bundles on the five newest releases, which they carry since 2026-10-11 | | `test_statement_coverage90` | Gold MUST | 89.41% statement coverage measured on 2026-10-11 (`test_statement_coverage80` and `test_branch_coverage80` are met at 89.41% and 84.84%) | A scenario for the `--help`, `--version` and invalid `--log-level` paths of `src/PostgreSqlMcp/Program.cs` (53 of its 105 lines are unexecuted), or other real scenarios from the `coverage` command's unexecuted list, until the total passes 90% | | `build_reproducible` | Gold MUST | Every asset except the provenance bundle is byte-identical across same-toolchain rebuilds, but the deterministic `.nupkg` and SBOM have not shipped in a release yet | After the next release, rebuild it independently and match every `SHA256SUMS` entry except `provenance.sigstore.json` | | `version_tags_signed` | Silver SUGGESTED | Tags are annotated but unsigned (v0.3.3 is lightweight) | Sign release tags (`git tag -s` with a signing key registered on GitHub) and document `git tag -v` | @@ -219,31 +219,29 @@ Silver still fails on one MUST criterion, `access_continuity`, so `achieve_silve ### Owner action checklist -Nothing here has been done by the change that proposes it; each item changes a remote setting or a published release and needs the owner. +Each item changes a remote setting or a published release, or needs the owner personally. Items 4, 5 and 10 were carried out or checked on 2026-10-11 by an AI coding agent under the maintainer's explicit authorization for that task; the results are recorded in each item. The rest are still open. 1. **Review and merge the pull request** that adds `GOVERNANCE.md`, `docs/architecture.md`, `docs/roadmap.md`, `docs/assurance-case.md`, the coding and review standards, the SPDX headers, `.editorconfig` with its CI step, deterministic `.nupkg`/SBOM output, the coverlet coverage gate, verified TLS by default (breaking) and the answer proposals. The proposal evidence URLs resolve only after the merge. 2. **Read `docs/assurance-case.md` and record your review** in its review record. Confirm or correct the roadmap and governance statements, which are drafts written for the maintainer's approval. 3. **Save the answers on bestpractices.dev.** Sign in as the owner and open the [silver](https://www.bestpractices.dev/en/projects/15155/silver/edit) and [gold](https://www.bestpractices.dev/en/projects/15155/gold/edit) sections. Use **Save (and continue) 🤖** so the site loads the repository proposals from `.bestpractices.json`, review every proposed answer against the evidence, and change any you disagree with. Answer `require_2FA`, `secure_2FA` and `security_review` yourself. -4. **Attach the existing provenance bundles to the older releases in Scorecard's window** (v0.3.1, v0.3.2, v0.3.3 and v0.4.0; v0.2.0 is already outside the window, and v0.1.0 has no attestation). Editing a published release is reserved for the owner. On 2026-10-11 the read-only first steps below were run for v0.4.0: its attestation was retrieved as one Sigstore bundle holding one SLSA v1 statement with nine subjects. The verification and upload steps were not run, and the `gh` version used there has no `gh attestation` command, so use a current one: +4. **Done 2026-10-11: existing provenance bundles attached to the older releases in Scorecard's window** (v0.3.1, v0.3.2, v0.3.3 and v0.4.0; v0.2.0 is already outside the window, and v0.1.0 has no attestation). For each tag, the attestation for its `SHA256SUMS` digest was retrieved from GitHub's attestation store as one Sigstore bundle with one SLSA v1 statement (`release.yml`, `refs/heads/main`). Its nine subjects matched the nine release assets name for name and digest for digest, `sha256sum --check SHA256SUMS` passed, and `gh attestation verify --bundle provenance.sigstore.json --signer-workflow codegiveness/postgresql-sharp-mcp/.github/workflows/release.yml --source-ref refs/heads/main` (GitHub CLI 2.102.0) passed for all nine assets. Only then was the bundle uploaded as `provenance.sigstore.json`; after the upload, the attached asset was downloaded again and the same checks passed for every asset of all four releases. To repeat the check for one release: ```bash mkdir v0.4.0 && cd v0.4.0 - gh release download v0.4.0 --repo codegiveness/postgresql-sharp-mcp --pattern SHA256SUMS - D=$(sha256sum SHA256SUMS | cut -d' ' -f1) - gh api "repos/codegiveness/postgresql-sharp-mcp/attestations/sha256:$D" --jq '.attestations[0].bundle' > provenance.sigstore.json - gh attestation verify SHA256SUMS --repo codegiveness/postgresql-sharp-mcp \ + gh release download v0.4.0 --repo codegiveness/postgresql-sharp-mcp + sha256sum --check SHA256SUMS + for f in *; do [ "$f" = provenance.sigstore.json ] || gh attestation verify "$f" \ + --repo codegiveness/postgresql-sharp-mcp \ --signer-workflow codegiveness/postgresql-sharp-mcp/.github/workflows/release.yml \ - --bundle provenance.sigstore.json - gh release upload v0.4.0 provenance.sigstore.json --repo codegiveness/postgresql-sharp-mcp + --source-ref refs/heads/main --bundle provenance.sigstore.json; done ``` - Upload only after verification passes, and repeat for the other three tags. Check that the bundle's subjects match that release's asset digests first. -5. **Label real small tasks.** Open or label two or three small issues `good first issue` (for example, add a direct `SqlGuard` verifier scenario, make constructor style consistent in `DatabaseTools`, or document the `ConfigureAwait` convention). This is what `small_tasks` asks for. +5. **Label real small tasks.** Checked 2026-10-11: the repository had no open issues (0 open, 133 closed), so there was nothing to label. When a genuinely small task comes up (for example, add a direct `SqlGuard` verifier scenario, make constructor style consistent in `DatabaseTools`, or document the `ConfigureAwait` convention), open it as an issue and label it `good first issue`. This is what `small_tasks` asks for; do not open issues only to satisfy the criterion. 6. **Release the TLS default as a breaking version.** Verified TLS by default changes behavior for remote profiles that omit `SSL Mode`; the CHANGELOG entry carries the upgrade and rollback note (`SSL Mode=Prefer`). Choose the next 0.x minor version for it ([versioning](../CONTRIBUTING.md#automation-and-releases)) and dispatch the release yourself. 7. **Optional: sign release tags** with a signing key registered on GitHub, which would settle `version_tags_signed`. 8. **Set up access continuity.** Route 2 in [GOVERNANCE.md](../GOVERNANCE.md#what-would-change-the-status) needs no second maintainer. Give a named trusted person sealed recovery material for the GitHub, npm and NuGet accounts and written authority to use it, then record the date in GOVERNANCE.md and change `access_continuity` to Met with that evidence. Never put the material in the repository. Optionally also set a GitHub account successor. 9. **Optional: recruit a second maintainer** (route 1). This is the only path to `bus_factor`, `two_person_review`, Code-Review, Contributors and a higher Branch-Protection score. No artificial reviews or contributors are used. -10. **Check the settings you own.** GitHub's secret-scanning generic patterns and validity checks were still reported disabled on 2026-10-11 ([enrollment](#openssf-best-practices-enrollment) and the 2026-10-02 note above); enabling them is a settings change for the owner. +10. **Check the settings you own.** Read on 2026-10-11 through `gh api repos/codegiveness/postgresql-sharp-mcp --jq .security_and_analysis`, without changing anything: secret scanning enabled, push protection enabled, Dependabot security updates enabled, non-provider (generic) patterns **disabled**, validity checks **disabled**; the open secret-scanning alert count was 0. Enabling generic patterns and validity checks is a settings change for the owner. ## Sources