Files
lmxopcua/tests/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests/CredentialPolicyReviewTests.cs
T
Joseph Doherty 271b96a644 fix(security): #835/#836 review — unsaved endpoints never borrow the owner's secrets; audit credential use; path rule follows AuthType
I-1: Test Connect / browse configs may carry unsaved edits (the device modal merges
the stored DriverConfig with the typed device config). The server
(DriverInstanceIdentities.EffectiveOwnerAsync, used by AdminOperationsActor and
BrowserSessionService) honours the named owner only when every endpoint value
(new DriverCredentialRules.EndpointFields: Galaxy Endpoint, OpcUaClient
EndpointUrl/EndpointUrls/DiscoveryUrl, Heidenhain Host/Port) is one that driver
stores; otherwise the owner is null and its instance-named secrets are refused.
Every Test Connect / browse whose config carries an in-scope secret:/env: reference
is recorded (CredentialUseAudit: IAuditWriter event category CredentialUse + a
structured Information line: user, driver, endpoint hosts, outcome).

L-1: the UserCertificatePath rule is skipped only in an object that explicitly sets
AuthType to non-Certificate (string or ordinal); the deploy gate also checks the
runtime-merged driver+device config so the switch cannot be split from the path; the
form no longer persists the hidden field.
L-2: a ConfigDb fault in the browse check is a refusal; RawBrowseModal catches open
failures into its error message.
L-4: a relative UserCertificateDirectory resolves against AppContext.BaseDirectory.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-11-11 04:12:40 -05:00

55 lines
3.2 KiB
C#

using Shouldly;
using Xunit;
namespace ZB.MOM.WW.OtOpcUa.Core.Abstractions.Tests;
/// <summary>#835/#836 security review: endpoint matching (I-1), credential-reference detection (audit) and L-1.</summary>
[Trait("Category", "Unit")]
public sealed class CredentialPolicyReviewTests
{
[Theory]
[InlineData("""{"AuthType":"Anonymous","UserCertificatePath":"C:\\x.pfx"}""", 0)]
[InlineData("""{"AuthType":"username","UserCertificatePath":"/etc/x.pfx"}""", 0)]
[InlineData("""{"AuthType":0,"UserCertificatePath":"/etc/x.pfx"}""", 0)]
[InlineData("""{"AuthType":"Certificate","UserCertificatePath":"/etc/x.pfx"}""", 1)]
[InlineData("""{"authType":2,"UserCertificatePath":"/etc/x.pfx"}""", 1)]
[InlineData("""{"UserCertificatePath":"/etc/x.pfx"}""", 1)] // no switch here — the merge may supply it
[InlineData("""{"AuthType":null,"UserCertificatePath":"/etc/x.pfx"}""", 1)]
public void L1_the_path_rule_applies_unless_the_object_explicitly_switches_certificate_auth_off(string config, int expected) =>
DriverCredentialPolicy.DescribeProblems(DriverTypeNames.OpcUaClient, config).Count.ShouldBe(expected);
[Fact]
public void Endpoint_values_are_collected_at_any_depth_and_flattened()
{
DriverCredentialPolicy.EndpointValues(DriverTypeNames.OpcUaClient,
"""{"EndpointUrl":" opc.tcp://a:4840 ","EndpointUrls":["opc.tcp://b:4840"],"Devices":[{"discoveryUrl":"opc.tcp://c"}]}""")
.ShouldBe(["opc.tcp://a:4840", "opc.tcp://b:4840", "opc.tcp://c"]);
DriverCredentialPolicy.EndpointValues(DriverTypeNames.Heidenhain, """{"Devices":[{"Host":"cnc1","Port":19000}]}""")
.ShouldBe(["cnc1", "19000"]);
DriverCredentialPolicy.EndpointValues(DriverTypeNames.Modbus, """{"Host":"x"}""").ShouldBeEmpty();
}
[Theory]
[InlineData("""{"Gateway":{"Endpoint":"http://gw:5001"}}""", true)]
[InlineData("""{"Gateway":{"Endpoint":"HTTP://GW:5001"}}""", true)]
[InlineData("""{"Gateway":{}}""", true)]
[InlineData("""{"Gateway":{"Endpoint":"http://attacker:5001"}}""", false)]
public void An_unsaved_endpoint_does_not_match_the_stored_driver(string submitted, bool expected) =>
DriverCredentialPolicy.EndpointsMatchStored(DriverTypeNames.Galaxy, submitted,
["""{"Gateway":{"ApiKeySecretRef":"secret:galaxy/a/k"}}""", """{"Gateway":{"Endpoint":"http://gw:5001"}}"""])
.ShouldBe(expected);
[Fact]
public void A_type_without_endpoint_fields_never_matches() =>
DriverCredentialPolicy.EndpointsMatchStored(DriverTypeNames.Modbus, "{}", ["{}"]).ShouldBeFalse();
[Theory]
[InlineData("""{"Gateway":{"ApiKeySecretRef":"secret:galaxy/x"}}""", true)]
[InlineData("""{"Gateway":{"ApiKeySecretRef":"env:OTOPCUA_GALAXY_X"}}""", true)]
[InlineData("""{"Gateway":{"ApiKeySecretRef":"dev:k"}}""", false)]
[InlineData("""{"Gateway":{"ApiKeySecretRef":"literal"}}""", false)]
[InlineData("""{"Gateway":{"ApiKeySecretRef":"env:ConnectionStrings__ConfigDb"}}""", false)] // out of scope: refused anyway
public void HasCredentialReferences_detects_resolvable_references(string config, bool expected) =>
DriverCredentialPolicy.HasCredentialReferences(DriverTypeNames.Galaxy, config).ShouldBe(expected);
}