Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 23 additions & 3 deletions Core/Resgrid.Config/McpConfig.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
namespace Resgrid.Config
namespace Resgrid.Config
{
/// <summary>
/// Configuration settings for the Model Context Protocol (MCP) server
Expand All @@ -17,9 +17,29 @@ public static class McpConfig
public static string ServerVersion = "1.0.0";

/// <summary>
/// The transport mechanism for MCP (e.g., "stdio")
/// The transport mechanism for MCP (e.g., "stdio", "http")
/// </summary>
public static string Transport = "stdio";
public static string Transport = "http";

/// <summary>
/// Enable CORS for HTTP transport (allows cross-origin requests)
/// </summary>
public static bool EnableCors = true;

/// <summary>
/// Allowed CORS origins (comma-separated list). Empty or "*" allows all origins.
/// </summary>
public static string CorsAllowedOrigins = "*";

/// <summary>
/// Enable HTTP transport endpoint
/// </summary>
public static bool EnableHttpTransport = true;

/// <summary>
/// Enable stdio transport (for backwards compatibility)
/// </summary>
public static bool EnableStdioTransport = false;
}
}

Expand Down
3 changes: 2 additions & 1 deletion Tests/Resgrid.Tests/Resgrid.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,12 @@
<ProjectReference Include="..\..\Providers\Resgrid.Providers.Number\Resgrid.Providers.Number.csproj" />
<ProjectReference Include="..\..\Providers\Resgrid.Providers.Pdf\Resgrid.Providers.Pdf.csproj" />
<ProjectReference Include="..\..\Repositories\Resgrid.Repositories.DataRepository\Resgrid.Repositories.DataRepository.csproj" />
<ProjectReference Include="..\..\Web\Resgrid.Web.Mcp\Resgrid.Web.Mcp.csproj" />
<ProjectReference Include="..\..\Workers\Resgrid.Workers.Framework\Resgrid.Workers.Framework.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="Data\TestCall.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
</Project>
208 changes: 208 additions & 0 deletions Tests/Resgrid.Tests/Web/Mcp/SensitiveDataRedactorTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
using NUnit.Framework;
using Resgrid.Web.Mcp.Infrastructure;

namespace Resgrid.Tests.Web.Mcp
{
/// <summary>
/// Unit tests for SensitiveDataRedactor to verify sensitive field redaction
/// </summary>
[TestFixture]
public sealed class SensitiveDataRedactorTests
{
private const string RedactedValue = "***REDACTED***";

[Test]
public void RedactSensitiveFields_ShouldRedactPassword()
{
// Arrange
var jsonWithPassword = @"{""username"":""john.doe"",""password"":""secret123""}";

// Act
var redacted = SensitiveDataRedactor.RedactSensitiveFields(jsonWithPassword);

// Assert
Assert.That(redacted, Does.Contain(RedactedValue), "Redacted output should contain redaction marker");
Assert.That(redacted, Does.Not.Contain("secret123"), "Redacted output should not contain original password");
Assert.That(redacted, Does.Not.Contain("john.doe"), "Redacted output should not contain username value");
}

[Test]
public void RedactSensitiveFields_ShouldRedactTokenAndApiKey()
{
// Arrange
var jsonWithToken = @"{""token"":""Bearer abc123"",""apikey"":""xyz789"",""data"":""safe data""}";

// Act
var redacted = SensitiveDataRedactor.RedactSensitiveFields(jsonWithToken);

// Assert
Assert.That(redacted, Does.Contain(RedactedValue), "Redacted output should contain redaction marker");
Assert.That(redacted, Does.Not.Contain("Bearer abc123"), "Redacted output should not contain original token");
Assert.That(redacted, Does.Not.Contain("xyz789"), "Redacted output should not contain original API key");
Assert.That(redacted, Does.Contain("safe data"), "Redacted output should preserve non-sensitive data");
}

[Test]
public void RedactSensitiveFields_ShouldRedactNestedSensitiveFields()
{
// Arrange
var nestedJson = @"{""user"":{""username"":""jane"",""password"":""pass456""},""sessionToken"":""token123""}";

// Act
var redacted = SensitiveDataRedactor.RedactSensitiveFields(nestedJson);

// Assert
Assert.That(redacted, Does.Contain(RedactedValue), "Redacted output should contain redaction marker");
Assert.That(redacted, Does.Not.Contain("jane"), "Redacted output should not contain nested username");
Assert.That(redacted, Does.Not.Contain("pass456"), "Redacted output should not contain nested password");
Assert.That(redacted, Does.Not.Contain("token123"), "Redacted output should not contain session token");
}

[Test]
public void RedactSensitiveFields_ShouldRedactJsonRpcRequest()
{
// Arrange
var jsonRpcRequest = @"{""jsonrpc"":""2.0"",""method"":""authenticate"",""params"":{""username"":""admin"",""password"":""admin123""},""id"":1}";

// Act
var redacted = SensitiveDataRedactor.RedactSensitiveFields(jsonRpcRequest);

// Assert
Assert.That(redacted, Does.Contain(RedactedValue), "Redacted output should contain redaction marker");
Assert.That(redacted, Does.Not.Contain("admin123"), "Redacted output should not contain password from params");
Assert.That(redacted, Does.Not.Contain("admin"), "Redacted output should not contain username from params");
Assert.That(redacted, Does.Contain("authenticate"), "Redacted output should preserve method name");
Assert.That(redacted, Does.Contain("2.0"), "Redacted output should preserve jsonrpc version");
}

[Test]
public void RedactSensitiveFields_ShouldHandleInvalidJson()
{
// Arrange
var invalidJson = "not valid json {";

// Act
var redacted = SensitiveDataRedactor.RedactSensitiveFields(invalidJson);

// Assert
Assert.That(redacted, Is.Not.Null, "Should return non-null result for invalid JSON");
Assert.That(redacted, Is.Not.Empty, "Should return non-empty result for invalid JSON");
Assert.That(redacted, Does.Not.Contain("not valid json"), "Should not contain original invalid JSON content");
}

[Test]
public void RedactSensitiveFields_ShouldHandleEmptyString()
{
// Arrange
var emptyJson = string.Empty;

// Act
var redacted = SensitiveDataRedactor.RedactSensitiveFields(emptyJson);

// Assert
Assert.That(redacted, Is.Empty, "Should return empty string for empty input");
}

[Test]
public void RedactSensitiveFields_ShouldHandleNullString()
{
// Arrange
string nullJson = null;

// Act
var redacted = SensitiveDataRedactor.RedactSensitiveFields(nullJson);

// Assert
Assert.That(redacted, Is.Empty, "Should return empty string for null input");
}

[Test]
public void RedactSensitiveFields_ShouldPreserveNonSensitiveFields()
{
// Arrange
var jsonWithMixedFields = @"{""id"":42,""name"":""John"",""email"":""john@example.com"",""status"":""active""}";

// Act
var redacted = SensitiveDataRedactor.RedactSensitiveFields(jsonWithMixedFields);

// Assert
Assert.That(redacted, Does.Contain(RedactedValue), "Should redact email field");
Assert.That(redacted, Does.Not.Contain("john@example.com"), "Should not contain original email");
Assert.That(redacted, Does.Contain("42"), "Should preserve id field");
Assert.That(redacted, Does.Contain("John"), "Should preserve name field");
Assert.That(redacted, Does.Contain("active"), "Should preserve status field");
}

[Test]
public void RedactSensitiveFields_ShouldRedactArraysWithSensitiveData()
{
// Arrange
var jsonWithArray = @"{""users"":[{""username"":""user1"",""password"":""pass1""},{""username"":""user2"",""password"":""pass2""}]}";

// Act
var redacted = SensitiveDataRedactor.RedactSensitiveFields(jsonWithArray);

// Assert
Assert.That(redacted, Does.Contain(RedactedValue), "Should redact sensitive fields in array");
Assert.That(redacted, Does.Not.Contain("user1"), "Should not contain first username");
Assert.That(redacted, Does.Not.Contain("user2"), "Should not contain second username");
Assert.That(redacted, Does.Not.Contain("pass1"), "Should not contain first password");
Assert.That(redacted, Does.Not.Contain("pass2"), "Should not contain second password");
}

[Test]
public void RedactSensitiveFields_ShouldRedactCommonSensitiveFieldNames()
{
// Arrange
var jsonWithVariousSensitiveFields = @"{
""password"":""secret"",
""token"":""token123"",
""ssn"":""123-45-6789"",
""apikey"":""key123"",
""api_key"":""key456"",
""secret"":""secret789"",
""authorization"":""Bearer xyz"",
""auth"":""auth123"",
""credentials"":""creds"",
""credit_card"":""4111111111111111"",
""creditcard"":""4111111111111111"",
""cvv"":""123"",
""pin"":""1234""
}";

// Act
var redacted = SensitiveDataRedactor.RedactSensitiveFields(jsonWithVariousSensitiveFields);

// Assert
Assert.That(redacted, Does.Contain(RedactedValue), "Should contain redaction marker");
Assert.That(redacted, Does.Not.Contain("secret"), "Should not contain 'secret' value");
Assert.That(redacted, Does.Not.Contain("token123"), "Should not contain token value");
Assert.That(redacted, Does.Not.Contain("123-45-6789"), "Should not contain SSN");
Assert.That(redacted, Does.Not.Contain("key123"), "Should not contain apikey value");
Assert.That(redacted, Does.Not.Contain("key456"), "Should not contain api_key value");
Assert.That(redacted, Does.Not.Contain("secret789"), "Should not contain secret value");
Assert.That(redacted, Does.Not.Contain("Bearer xyz"), "Should not contain authorization value");
Assert.That(redacted, Does.Not.Contain("auth123"), "Should not contain auth value");
Assert.That(redacted, Does.Not.Contain("creds"), "Should not contain credentials value");
Assert.That(redacted, Does.Not.Contain("4111111111111111"), "Should not contain credit card number");
Assert.That(redacted, Does.Not.Contain("1234"), "Should not contain PIN");
}
Comment on lines +153 to +189
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Line 178 assertion will fail — "secret" is also a JSON property name in the output.

The redactor replaces values of sensitive fields but keeps the property names. The redacted output will contain "secret":"***REDACTED***", so Does.Not.Contain("secret") fails as a substring match against the key.

Use a more targeted assertion that checks the value was replaced rather than doing a blanket substring search for a word that also appears as a key name.

🐛 Proposed fix
-			Assert.That(redacted, Does.Not.Contain("secret"), "Should not contain 'secret' value");
+			Assert.That(redacted, Does.Not.Contain(@"""secret"""), "Should not contain unredacted 'secret' value");

Alternatively, deserialize the redacted JSON and assert the property value directly equals "***REDACTED***" — this avoids fragile substring checks entirely.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Tests/Resgrid.Tests/Web/Mcp/SensitiveDataRedactorTests.cs` around lines 153 -
189, The test currently asserts that the redacted string
Does.Not.Contain("secret") which fails because "secret" is also a JSON property
name; update RedactSensitiveFields_ShouldRedactCommonSensitiveFieldNames to
deserialize the result of
SensitiveDataRedactor.RedactSensitiveFields(jsonWithVariousSensitiveFields) (or
parse it into a JObject) and assert that the sensitive properties (e.g.
"secret", "password", "token", "ssn", "apikey", "api_key", "authorization",
"auth", "credentials", "credit_card"/"creditcard", "cvv", "pin") have values
equal to RedactedValue (or that jObject["secret"].Value<string>() ==
RedactedValue) instead of using Does.Not.Contain substring checks.


[Test]
public void RedactSensitiveFields_ShouldHandleCaseInsensitiveFieldNames()
{
// Arrange
var jsonWithMixedCase = @"{""Password"":""secret"",""TOKEN"":""token123"",""ApiKey"":""key456""}";

// Act
var redacted = SensitiveDataRedactor.RedactSensitiveFields(jsonWithMixedCase);

// Assert
Assert.That(redacted, Does.Contain(RedactedValue), "Should redact case-insensitive field names");
Assert.That(redacted, Does.Not.Contain("secret"), "Should not contain password value");
Assert.That(redacted, Does.Not.Contain("token123"), "Should not contain token value");
Assert.That(redacted, Does.Not.Contain("key456"), "Should not contain API key value");
}
}
}

1 change: 1 addition & 0 deletions Web/Resgrid.Web.Mcp/Controllers/HealthController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ public HealthController(
/// </summary>
/// <returns>HealthResult object with the server health status</returns>
[HttpGet("current")]
[HttpGet("getcurrent")]
public async Task<IActionResult> GetCurrent()
{
var result = new HealthResult
Expand Down
Loading
Loading