add value-shape field classification and auto-apply
Field roles are now inferred from observed values instead of names, with src/dst disambiguation, value-pattern detector hints, an Ollama advisor scoped to ambiguous fields only, confidence-gated auto-apply with manual-field protection, and drift flagging for review
This commit is contained in:
@@ -276,6 +276,39 @@ class ProfileSuggestionTests(unittest.TestCase):
|
||||
self.assertEqual(advised["profile_advisor"]["field_roles"], {"username": "identity"})
|
||||
self.assertEqual(advised["profile_advisor"]["correlation_roles"], {"identity": ["username"]})
|
||||
|
||||
def test_scoped_field_roles_patch_only_the_named_fields(self):
|
||||
suggestion = suggest_stream_profiles([
|
||||
parse_log_line("fgai_stream_id=windows fgai_stream=Windows username=alice hostname=host01 eventid=4625 action=failure")
|
||||
])[0]
|
||||
|
||||
advised = apply_profile_advice([suggestion], [{
|
||||
"stream_id": "windows",
|
||||
"field_roles": {"hostname": "entity", "action": "reject"},
|
||||
"reason": "scoped advisor pass",
|
||||
}])[0]
|
||||
|
||||
self.assertEqual(advised["profile_advisor"]["status"], "ok")
|
||||
self.assertEqual(advised["profile_advisor"]["fields_reclassified"], 2)
|
||||
self.assertIn("username", advised["profile"]["entity_fields"])
|
||||
self.assertIn("hostname", advised["profile"]["entity_fields"])
|
||||
self.assertNotIn("action", advised["profile"]["categorical_fields"])
|
||||
|
||||
def test_scoped_advisor_never_rejects_the_only_entity_field(self):
|
||||
events = [
|
||||
parse_log_line(f"fgai_stream_id=custom_vendor fgai_stream=CustomVendor x7_field_a=203.0.113.{index}")
|
||||
for index in range(1, 25)
|
||||
]
|
||||
suggestion = suggest_stream_profiles(events)[0]
|
||||
self.assertEqual(suggestion["profile"]["entity_fields"], ["x7_field_a"])
|
||||
|
||||
advised = apply_profile_advice([suggestion], [{
|
||||
"stream_id": "custom_vendor",
|
||||
"field_roles": {"x7_field_a": "reject"},
|
||||
"reason": "scoped advisor pass",
|
||||
}])[0]
|
||||
|
||||
self.assertIn("x7_field_a", advised["profile"]["entity_fields"])
|
||||
|
||||
def test_missing_llm_advice_keeps_heuristic_profile(self):
|
||||
suggestion = suggest_stream_profiles([
|
||||
parse_log_line("fgai_stream_id=app fgai_stream=App actor_id=user1 action=login")
|
||||
@@ -286,6 +319,64 @@ class ProfileSuggestionTests(unittest.TestCase):
|
||||
self.assertEqual(advised["profile_advisor"]["status"], "heuristic")
|
||||
self.assertEqual(advised["profile"], suggestion["profile"])
|
||||
|
||||
def test_unfamiliar_field_name_selected_as_entity_from_value_shape_alone(self):
|
||||
events = [
|
||||
parse_log_line(
|
||||
f"fgai_stream_id=custom_vendor fgai_stream=CustomVendor x7_field_a=203.0.113.{index} "
|
||||
f"x7_field_b=some-{index % 5} timestamp=2026-06-29T10:00:{index:02d}Z"
|
||||
)
|
||||
for index in range(1, 25)
|
||||
]
|
||||
|
||||
suggestion = suggest_stream_profiles(events)[0]
|
||||
|
||||
self.assertIn("x7_field_a", suggestion["profile"]["entity_fields"])
|
||||
self.assertEqual(suggestion["field_shapes"]["x7_field_a"], "ip")
|
||||
self.assertGreaterEqual(suggestion["field_confidence"]["x7_field_a"], 60)
|
||||
|
||||
def test_ambiguous_ip_fields_disambiguated_by_private_ratio(self):
|
||||
events = [
|
||||
parse_log_line(
|
||||
f"fgai_stream_id=custom_vendor fgai_stream=CustomVendor field_alpha=10.0.0.{index} "
|
||||
f"field_beta=8.8.8.{index} timestamp=2026-06-29T10:00:{index:02d}Z"
|
||||
)
|
||||
for index in range(1, 25)
|
||||
]
|
||||
|
||||
suggestion = suggest_stream_profiles(events)[0]
|
||||
relationships = suggestion["profile"]["relationship_fields"]
|
||||
|
||||
pairs = {(item["left"], item["right"]) for item in relationships}
|
||||
self.assertIn(("field_alpha", "field_beta"), pairs)
|
||||
|
||||
def test_deny_action_detector_suggested_from_value_pattern_alone(self):
|
||||
events = [
|
||||
parse_log_line(
|
||||
f"fgai_stream_id=custom_vendor fgai_stream=CustomVendor whatever_field="
|
||||
f"{'Denied' if index % 3 else 'Blocked'} src=10.0.0.{index} timestamp=2026-06-29T10:00:{index:02d}Z"
|
||||
)
|
||||
for index in range(1, 25)
|
||||
]
|
||||
|
||||
suggestion = suggest_stream_profiles(events)[0]
|
||||
detectors = suggestion["profile"]["detectors"]
|
||||
|
||||
self.assertIn("deny_action", detectors)
|
||||
self.assertEqual(detectors["deny_action"].get("source"), "value_shape")
|
||||
|
||||
def test_every_suggestion_carries_field_confidence_and_shapes(self):
|
||||
events = [
|
||||
parse_log_line(f"fgai_stream_id=firewall fgai_stream=Firewall srcip=10.0.0.{index} dstip=8.8.8.8 dstport=443 action=deny timestamp=2026-06-29T10:00:{index:02d}Z hitcount={index}")
|
||||
for index in range(1, 25)
|
||||
]
|
||||
|
||||
suggestion = suggest_stream_profiles(events)[0]
|
||||
|
||||
self.assertIn("field_confidence", suggestion)
|
||||
self.assertIn("field_shapes", suggestion)
|
||||
self.assertIn("srcip", suggestion["field_shapes"])
|
||||
self.assertEqual(suggestion["field_shapes"]["srcip"], "ip")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user