import unittest from fgai.logs import parse_log_line from fgai.profile_suggestions import apply_profile_advice, suggest_stream_profiles class ProfileSuggestionTests(unittest.TestCase): def test_suggests_common_profile_fields_per_stream(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) ] suggestions = suggest_stream_profiles(events) profile = suggestions[0]["profile"] self.assertEqual(profile["stream_id"], "firewall") self.assertIn("srcip", profile["entity_fields"]) self.assertEqual(profile["timestamp_field"], "eventtime") self.assertIn("action", profile["categorical_fields"]) self.assertIn("dstport", profile["categorical_fields"]) self.assertIn("hitcount", profile["numeric_fields"]) self.assertIn("deny_action", profile["detectors"]) def test_placeholder_winlog_fields_do_not_make_firewall_windows(self): events = [ parse_log_line( f"fgai_stream_id=firewall fgai_stream='Firewall Logs' srcip=10.0.0.{index} dstip=8.8.8.8 " f"dstport=443 action=deny policyid=12 eventid=- winlog.channel=- winlog.event_id=- " f"winlog.event_data.targetusername=- hostname=- username=- timestamp=2026-06-29T10:00:{index:02d}Z" ) for index in range(1, 25) ] suggestion = suggest_stream_profiles(events)[0] profile = suggestion["profile"] self.assertEqual(suggestion["detected_log_type"], "firewall") self.assertNotIn("username", profile["entity_fields"]) self.assertNotIn("hostname", profile["entity_fields"]) self.assertIn("srcip", profile["entity_fields"]) self.assertIn("policyid", profile["categorical_fields"]) self.assertIn("deny_action", profile["detectors"]) def test_suggests_windows_event_fields(self): events = [ parse_log_line( f"fgai_stream_id=windows fgai_stream='Illuminate:Windows Security Event Log Messages' winlog_event_id=4625 " f"winlog_event_data_targetusername=alice winlog_computer_name=host{index % 3} " f"event_provider=Microsoft-Windows-Security-Auditing channel=Security logon_type=3 " f"winlog_event_data_ipaddress=10.0.0.{index % 5} " f"eventtime=2026-06-29T10:00:{index:02d}Z action=failure" ) for index in range(1, 30) ] profile = suggest_stream_profiles(events)[0]["profile"] self.assertIn("username", profile["entity_fields"]) self.assertIn("hostname", profile["entity_fields"]) self.assertIn("srcip", profile["entity_fields"]) self.assertNotIn("winlog_event_data_targetusername", profile["entity_fields"]) self.assertIn("eventid", profile["categorical_fields"]) self.assertIn("logon_type", profile["categorical_fields"]) self.assertIn("event_provider", profile["categorical_fields"]) self.assertNotIn("policyid", profile["categorical_fields"]) self.assertNotIn("dstport", profile["categorical_fields"]) self.assertNotIn("srcip", profile["categorical_fields"]) self.assertNotIn("eventid", profile["numeric_fields"]) self.assertIn("auth_failure", profile["detectors"]) def test_windows_named_stream_gets_auth_profile_even_before_failures(self): events = [ parse_log_line( f"fgai_stream_id=winsec fgai_stream='Illuminate:Windows Security Event Log Messages' " f"eventid=4624 username=user{index % 4} hostname=host{index % 2} channel=Security " f"eventtime=2026-06-29T10:00:{index:02d}Z action=success" ) for index in range(1, 30) ] suggestion = suggest_stream_profiles(events)[0] profile = suggestion["profile"] self.assertEqual(suggestion["stream_name"], "Illuminate:Windows Security Event Log Messages") self.assertIn("username", profile["entity_fields"]) self.assertIn("hostname", profile["entity_fields"]) self.assertIn("eventid", profile["categorical_fields"]) self.assertIn("auth_failure", profile["detectors"]) def test_winlogbeat_ecs_fields_build_windows_profile(self): events = [ parse_log_line( f"fgai_stream_id=winbeat fgai_stream='Winlogbeat Security' " f"@timestamp=2026-06-30T10:00:{index:02d}Z event.code=4625 event.action=logon-failed " f"event.outcome=failure event.category=authentication event.type=start " f"user.name=user{index % 4} host.name=win{index % 3} source.ip=10.0.0.{index % 5} " f"winlog.channel=Security winlog.provider_name=Microsoft-Windows-Security-Auditing " f"winlog.event_data.LogonType=3 process.name=proc{index % 3}.exe" ) for index in range(1, 30) ] profile = suggest_stream_profiles(events)[0]["profile"] self.assertEqual(profile["timestamp_field"], "eventtime") self.assertEqual(profile["entity_fields"][:3], ["username", "hostname", "srcip"]) self.assertIn("eventid", profile["categorical_fields"]) self.assertIn("action", profile["categorical_fields"]) self.assertIn("event.category", profile["categorical_fields"]) self.assertIn("winlog.channel", profile["categorical_fields"]) self.assertIn("process.name", profile["categorical_fields"]) self.assertNotIn("user.name", profile["categorical_fields"]) self.assertNotIn("source.ip", profile["categorical_fields"]) self.assertIn("auth_failure", profile["detectors"]) def test_discovers_non_priority_application_fields(self): events = [ parse_log_line( f"fgai_stream_id=app fgai_stream=BillingApp tenant_id=t{index % 4} actor_id=user{index % 7} " f"workflow_state={'approved' if index % 2 else 'rejected'} payment_provider=stripe risk_points={index % 9} " f"created_at=2026-06-29T10:00:{index:02d}Z" ) for index in range(1, 40) ] profile = suggest_stream_profiles(events)[0]["profile"] self.assertIn("username", profile["entity_fields"]) self.assertIn("tenant_id", profile["categorical_fields"]) self.assertIn("workflow_state", profile["categorical_fields"]) self.assertIn("risk_points", profile["numeric_fields"]) def test_profile_discovery_explains_selected_and_rejected_fields(self): events = [ parse_log_line( f"fgai_stream_id=app fgai_stream=BillingApp tenant_id=t{index % 4} actor_id=user{index % 7} " f"workflow_state={'approved' if index % 2 else 'rejected'} request_id=req-{index} " f"constant_field=same raw='{{json}}' risk_points={index % 9} created_at=2026-06-29T10:00:{index:02d}Z" ) for index in range(1, 40) ] suggestion = suggest_stream_profiles(events)[0] discovery = suggestion["discovery"] self.assertEqual(suggestion["detected_log_type"], "application") self.assertEqual(discovery["log_type"], "application") self.assertIn("actor_id", discovery["selected_fields"]["entity"]) self.assertIn("workflow_state", discovery["selected_fields"]["categorical"]) self.assertIn("risk_points", discovery["selected_fields"]["numeric"]) rejected = {item["field"]: item["reason"] for item in discovery["rejected_fields"]} self.assertEqual(rejected["raw"], "raw/internal field") self.assertEqual(rejected["constant_field"], "constant value") self.assertEqual(rejected["request_id"], "too high cardinality") def test_fields_shared_between_streams_are_default_profile_candidates(self): events = [] for stream_id, stream_name in (("billing", "BillingApp"), ("orders", "OrdersApp")): events.extend( parse_log_line( f"fgai_stream_id={stream_id} fgai_stream={stream_name} actor_id=user{index % 6} " f"tenant_id=t{index % 3} workflow_state={'approved' if index % 2 else 'rejected'} " f"result_code=r{index % 4} latency_ms={index % 9} created_at=2026-06-29T10:00:{index:02d}Z" ) for index in range(1, 30) ) suggestions = {item["stream_id"]: item for item in suggest_stream_profiles(events)} for suggestion in suggestions.values(): profile = suggestion["profile"] shared = {item["field"] for item in suggestion["shared_fields"]} self.assertIn("workflow_state", shared) self.assertIn("result_code", shared) self.assertIn("latency_ms", shared) self.assertIn("workflow_state", profile["categorical_fields"]) self.assertIn("result_code", profile["categorical_fields"]) self.assertIn("latency_ms", profile["numeric_fields"]) def test_semantic_alias_fields_are_shared_across_streams(self): events = [] events.extend( parse_log_line( f"fgai_stream_id=proxy fgai_stream=Proxy source.ip=10.0.0.{index % 7} url=/item/{index % 3} action=allow" ) for index in range(1, 30) ) events.extend( parse_log_line( f"fgai_stream_id=firewall fgai_stream=Firewall client_ip=10.0.0.{index % 7} dstport=443 action=accept" ) for index in range(1, 30) ) suggestions = {item["stream_id"]: item for item in suggest_stream_profiles(events)} for suggestion in suggestions.values(): shared = {item["field"]: item for item in suggestion["shared_fields"]} self.assertIn("srcip", shared) self.assertEqual(shared["srcip"]["kind"], "entity") self.assertIn("source.ip", shared["srcip"]["aliases"]) self.assertIn("client_ip", shared["srcip"]["aliases"]) def test_custom_namespace_and_suffix_fields_are_shared_across_streams(self): events = [] for stream_id, field in (("app1", "lcs_customer_id"), ("app2", "lcs_order_id")): events.extend( parse_log_line( f"fgai_stream_id={stream_id} fgai_stream={stream_id} {field}=id{index % 4} result_state=ok actor_name=user{index % 3}" ) for index in range(1, 20) ) suggestions = {item["stream_id"]: item for item in suggest_stream_profiles(events)} for suggestion in suggestions.values(): shared = {item["field"]: item for item in suggestion["shared_fields"]} self.assertIn("lcs_*", shared) self.assertIn("*_id", shared) self.assertIn("*_status", shared) self.assertIn("*_name", shared) def test_heuristic_profile_recommends_behavior_relationships(self): events = [ parse_log_line( f"fgai_stream_id=windows fgai_stream=Windows username=user{index % 3} srcip=10.0.0.{index % 5} hostname=host{index % 4} action=success eventid=4624" ) for index in range(1, 40) ] suggestion = suggest_stream_profiles(events)[0] relationships = {(item["left"], item["right"]) for item in suggestion["relationship_fields"]} self.assertIn(("username", "srcip"), relationships) self.assertIn("username->srcip", suggestion["discovery"]["selected_fields"]["relationships"]) self.assertEqual(suggestion["profile"]["relationship_fields"], suggestion["relationship_fields"]) def test_heuristic_profile_recommends_generic_entity_behavior_relationships(self): events = [ parse_log_line( f"fgai_stream_id=app fgai_stream=App entry_context_clientipaddress=10.0.0.{index % 5} entry_context_type={'login' if index % 2 else 'update'} houston_category=cat{index % 3}" ) for index in range(1, 40) ] suggestion = suggest_stream_profiles(events)[0] relationships = {(item["left"], item["right"]) for item in suggestion["relationship_fields"]} self.assertIn(("entry_context_clientipaddress", "entry_context_type"), relationships) def test_applies_valid_llm_advice_and_rejects_unknown_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", "entity_fields": ["username", "not_a_field"], "timestamp_field": "eventtime", "categorical_fields": ["eventid", "full_message"], "numeric_fields": ["missing_number"], "detectors": {"auth_failure": {"enabled": True, "minimum": 3, "z_threshold": 2.5}, "made_up": {"enabled": True}}, "relationship_fields": [{"left": "username", "right": "eventid", "name": "user event"}, {"left": "username", "right": "not_a_field"}], "field_roles": {"username": "identity", "not_a_field": "entity"}, "correlation_roles": {"identity": ["username", "not_a_field"], "asset": ["missing_host"]}, "reason": "Windows auth fields", }])[0] self.assertEqual(advised["profile_advisor"]["status"], "ok") self.assertEqual(advised["profile"]["entity_fields"], ["username"]) self.assertIn("eventid", advised["profile"]["categorical_fields"]) self.assertNotIn("full_message", advised["profile"]["categorical_fields"]) self.assertEqual(set(advised["profile"]["detectors"]), {"auth_failure"}) self.assertEqual(advised["profile"]["relationship_fields"], [{"left": "username", "right": "eventid", "name": "user event"}]) self.assertEqual(advised["profile_advisor"]["field_roles"], {"username": "identity"}) self.assertEqual(advised["profile_advisor"]["correlation_roles"], {"identity": ["username"]}) 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") ])[0] advised = apply_profile_advice([suggestion], [])[0] self.assertEqual(advised["profile_advisor"]["status"], "heuristic") self.assertEqual(advised["profile"], suggestion["profile"]) if __name__ == "__main__": unittest.main()