mirror of
https://github.com/okieselbach/AutopilotMonitor.git
synced 2026-10-25 13:10:13 +00:00
98fd173fb8
Full verify pass over all 54 built-in rules (validate_rule sweep, 30d/7d
get_rule_stats, test_analyze_rule spot checks). Fixes:
- ANALYZE-APP-007 v1.1.0: declare enrollment_failed as a non-required
condition — the exists-factor referenced an undeclared signal and could
never apply.
- ANALYZE-APP-014 v1.1.0: add non-required event_data conditions for
espCategory/espErrorCode/topCandidatePackage/candidateCount/confidence
so all explanation {{tokens}} resolve instead of rendering literally.
- ANALYZE-ENRL-002 v1.2.0: capture session_timeout.timeoutHours so
{{timeoutHours}} interpolates (verified via dry-run: renders "5").
- RuleEngine TryFlatLookup/ResolveDotPath: flatten IEnumerable data
values to a comma-joined string — List fields (e.g. app_tracking_summary
.pendingNames) stringified to "System.Collections.Generic.List`1[…]",
breaking contains-matching and {{pendingNames}} interpolation.
- GetRuleStatsAsync: raise the 500-row cap to 10000 and log when hit.
The scan walks dates ascending, so the old cap silently dropped the
NEWEST dates of a 30d window — newly added rules (e.g. ANALYZE-SEC-008)
were entirely missing from global stats.
- MCP validate_rule lint: accept event_data_array itemField as a {{token}}
resolver — {{identity}} in ANALYZE-SEC-005/006 was a false positive.
Backend 4157 tests green, MCP 548 green + tsc clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
143 lines
4.9 KiB
C#
143 lines
4.9 KiB
C#
using System.Text.Json;
|
|
using AutopilotMonitor.Functions.Services;
|
|
|
|
namespace AutopilotMonitor.Functions.Tests;
|
|
|
|
/// <summary>
|
|
/// Unit tests for the dot-path resolver inside <see cref="RuleEngine.GetDataFieldValue"/>.
|
|
/// Exercises both nested <c>Dictionary<string,object></c> and <see cref="JsonElement"/>
|
|
/// payload shapes — both occur in production depending on whether the event came in via
|
|
/// the in-process emitter (typed sidecar) or was rehydrated from Table Storage JSON.
|
|
/// </summary>
|
|
public class RuleEngineDataPathResolverTests
|
|
{
|
|
[Fact]
|
|
public void TryFlatLookup_LiteralKeyWithDot_StillResolves()
|
|
{
|
|
// Pre-existing v1 events used keys like "scan_summary.critical_cves" with a literal
|
|
// dot. The flat lookup must keep working so legacy rules don't break.
|
|
var data = new Dictionary<string, object>
|
|
{
|
|
["scan_summary.critical_cves"] = 7,
|
|
};
|
|
|
|
var ok = RuleEngine.TryFlatLookup(data, "scan_summary.critical_cves", out var value);
|
|
|
|
Assert.True(ok);
|
|
Assert.Equal("7", value);
|
|
}
|
|
|
|
[Fact]
|
|
public void TryFlatLookup_CaseInsensitive()
|
|
{
|
|
var data = new Dictionary<string, object> { ["FailedSubcategories"] = "Certificates,VPN" };
|
|
|
|
var ok = RuleEngine.TryFlatLookup(data, "failedsubcategories", out var value);
|
|
|
|
Assert.True(ok);
|
|
Assert.Equal("Certificates,VPN", value);
|
|
}
|
|
|
|
[Fact]
|
|
public void ResolveDotPath_NestedDictionary_ReturnsLeafScalar()
|
|
{
|
|
var data = new Dictionary<string, object>
|
|
{
|
|
["scan_summary"] = new Dictionary<string, object>
|
|
{
|
|
["critical_cves"] = 3,
|
|
["overall_risk"] = "high",
|
|
},
|
|
};
|
|
|
|
Assert.Equal("3", RuleEngine.ResolveDotPath(data, "scan_summary.critical_cves"));
|
|
Assert.Equal("high", RuleEngine.ResolveDotPath(data, "scan_summary.overall_risk"));
|
|
}
|
|
|
|
[Fact]
|
|
public void ResolveDotPath_NestedJsonElement_ReturnsLeafScalar()
|
|
{
|
|
// Events read back from Table Storage hit the resolver as JsonElement subtrees.
|
|
var json = """{"scan_summary":{"critical_cves":5,"kev_matches":2,"overall_risk":"critical"}}""";
|
|
using var doc = JsonDocument.Parse(json);
|
|
var data = new Dictionary<string, object>
|
|
{
|
|
["scan_summary"] = doc.RootElement.GetProperty("scan_summary").Clone(),
|
|
};
|
|
|
|
Assert.Equal("5", RuleEngine.ResolveDotPath(data, "scan_summary.critical_cves"));
|
|
Assert.Equal("2", RuleEngine.ResolveDotPath(data, "scan_summary.kev_matches"));
|
|
Assert.Equal("critical", RuleEngine.ResolveDotPath(data, "scan_summary.overall_risk"));
|
|
}
|
|
|
|
[Fact]
|
|
public void ResolveDotPath_CaseInsensitiveAtEachLevel()
|
|
{
|
|
var data = new Dictionary<string, object>
|
|
{
|
|
["Scan_Summary"] = new Dictionary<string, object>
|
|
{
|
|
["Critical_CVEs"] = 4,
|
|
},
|
|
};
|
|
|
|
Assert.Equal("4", RuleEngine.ResolveDotPath(data, "scan_summary.critical_cves"));
|
|
}
|
|
|
|
[Fact]
|
|
public void ResolveDotPath_MissingPath_ReturnsNull()
|
|
{
|
|
var data = new Dictionary<string, object>
|
|
{
|
|
["scan_summary"] = new Dictionary<string, object> { ["critical_cves"] = 1 },
|
|
};
|
|
|
|
Assert.Null(RuleEngine.ResolveDotPath(data, "scan_summary.missing"));
|
|
Assert.Null(RuleEngine.ResolveDotPath(data, "missing.critical_cves"));
|
|
}
|
|
|
|
[Fact]
|
|
public void ResolveDotPath_DescendIntoScalar_ReturnsNull()
|
|
{
|
|
// Path traverses past a scalar — must not throw, must return null so the rule
|
|
// misses cleanly instead of stringifying an unrelated value.
|
|
var data = new Dictionary<string, object>
|
|
{
|
|
["scan_summary"] = "not-an-object",
|
|
};
|
|
|
|
Assert.Null(RuleEngine.ResolveDotPath(data, "scan_summary.critical_cves"));
|
|
}
|
|
|
|
[Fact]
|
|
public void TryFlatLookup_ListValue_FlattensToJoinedString()
|
|
{
|
|
// Regression: app_tracking_summary.pendingNames is a List<object> after DataJson
|
|
// deserialization. ToString() on it yields the .NET type name, which broke both
|
|
// contains-matching and {{pendingNames}} interpolation in ANALYZE-APP-015 findings.
|
|
var data = new Dictionary<string, object>
|
|
{
|
|
["pendingNames"] = new List<object> { "Contoso VPN", "Fabrikam Agent" },
|
|
};
|
|
|
|
var ok = RuleEngine.TryFlatLookup(data, "pendingNames", out var value);
|
|
|
|
Assert.True(ok);
|
|
Assert.Equal("Contoso VPN, Fabrikam Agent", value);
|
|
}
|
|
|
|
[Fact]
|
|
public void ResolveDotPath_ListLeaf_FlattensToJoinedString()
|
|
{
|
|
var data = new Dictionary<string, object>
|
|
{
|
|
["summary"] = new Dictionary<string, object>
|
|
{
|
|
["names"] = new List<object> { "a", "b" },
|
|
},
|
|
};
|
|
|
|
Assert.Equal("a, b", RuleEngine.ResolveDotPath(data, "summary.names"));
|
|
}
|
|
}
|