Add PII redaction POC for secure LLM prompting.

Implements detect-redact-sanitize pipeline with regex, domain rules, and ONNX NER before the LLM boundary, plus NUnit tests and Xenovex push documentation.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Bilal Nazer Ali
2026-07-07 13:05:07 +05:30
commit dfc81dea28
60 changed files with 3283 additions and 0 deletions

View File

@@ -0,0 +1,53 @@
using FluentAssertions;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using PiiRedaction.Core.Configuration;
using PiiRedaction.Core.Models;
using PiiRedaction.Infrastructure.Onnx;
namespace PiiRedaction.Infrastructure.Tests.Onnx;
[TestFixture]
public sealed class OnnxNerModelRunnerTests
{
[Test]
public void Constructor_MissingModel_IsModelUnavailable()
{
var path = Path.Combine(Path.GetTempPath(), $"missing-ner-{Guid.NewGuid():N}.onnx");
using var runner = CreateRunner(path);
runner.IsModelAvailable.Should().BeFalse();
}
[Test]
public void PredictEntities_WhenModelUnavailable_ReturnsEmpty()
{
var path = Path.Combine(Path.GetTempPath(), $"missing-ner-{Guid.NewGuid():N}.onnx");
using var runner = CreateRunner(path);
runner.PredictEntities("Customer Ravi Kumar").Should().BeEmpty();
}
[Test]
public void Constructor_InvalidModelFile_IsModelUnavailable()
{
var path = Path.Combine(Path.GetTempPath(), $"invalid-ner-{Guid.NewGuid():N}.onnx");
File.WriteAllText(path, "not-a-valid-onnx-model");
try
{
using var runner = CreateRunner(path);
runner.IsModelAvailable.Should().BeFalse();
}
finally
{
File.Delete(path);
}
}
private static OnnxNerModelRunner CreateRunner(string modelPath)
{
var options = Options.Create(new PiiRedactionOptions { OnnxModelPath = modelPath });
return new OnnxNerModelRunner(options, NullLogger<OnnxNerModelRunner>.Instance);
}
}

View File

@@ -0,0 +1,51 @@
using FluentAssertions;
using PiiRedaction.Core.Models;
using PiiRedaction.Tests.Shared;
namespace PiiRedaction.Infrastructure.Tests.Onnx;
[TestFixture]
[Category("RealModel")]
public sealed class RealNerModelRunnerTests : RealNerModelFixture
{
[TestCase("Customer Ravi Kumar called about billing.", "Ravi", "Ravi Kumar")]
[TestCase("Mr. John Smith called about a duplicate debit.", "John", "John Smith")]
public void PredictEntities_DetectsPersonWithCorrectSpan(string prompt, string expectedNamePart, string expectedValue)
{
var entities = Runner.PredictEntities(prompt);
entities.Should().Contain(entity =>
entity.Type == PiiEntityType.Person &&
entity.Source == PiiDetectionSource.Ner &&
entity.Value.Contains(expectedNamePart, StringComparison.Ordinal) &&
prompt.AsSpan(entity.StartIndex, entity.Length).ToString() == entity.Value);
entities.Should().Contain(entity => entity.Value == expectedValue);
}
}