@@ -100,7 +100,7 @@ def test_get_resource_timeline_with_resource_id(
100100
101101 assert len (result ) == 1
102102 assert result [0 ]["event_name" ] == "RunInstances"
103- assert result [0 ]["actor" ] == "admin"
103+ assert result [0 ]["actor" ] == "user/ admin"
104104 assert result [0 ]["source_ip_address" ] == "203.0.113.1"
105105
106106 def test_get_resource_timeline_with_resource_uid (
@@ -304,14 +304,28 @@ def test_extract_actor_iam_user(self):
304304 "arn" : "arn:aws:iam::123456789012:user/alice" ,
305305 "userName" : "alice" ,
306306 }
307- assert CloudTrailTimeline ._extract_actor (user_identity ) == "alice"
307+ assert CloudTrailTimeline ._extract_actor (user_identity ) == "user/ alice"
308308
309309 def test_extract_actor_assumed_role (self ):
310310 user_identity = {
311311 "type" : "AssumedRole" ,
312312 "arn" : "arn:aws:sts::123456789012:assumed-role/MyRole/session-name" ,
313313 }
314- assert CloudTrailTimeline ._extract_actor (user_identity ) == "MyRole"
314+ assert (
315+ CloudTrailTimeline ._extract_actor (user_identity )
316+ == "assumed-role/MyRole/session-name"
317+ )
318+
319+ def test_extract_actor_assumed_role_sso (self ):
320+ """SSO sessions store the user identity in the session name."""
321+ user_identity = {
322+ "type" : "AssumedRole" ,
323+ "arn" : "arn:aws:sts::123456789012:assumed-role/AWSReservedSSO_AdministratorAccess_abcdef1234567890/user@example.com" ,
324+ }
325+ assert (
326+ CloudTrailTimeline ._extract_actor (user_identity )
327+ == "assumed-role/AWSReservedSSO_AdministratorAccess_abcdef1234567890/user@example.com"
328+ )
315329
316330 def test_extract_actor_root (self ):
317331 user_identity = {"type" : "Root" , "arn" : "arn:aws:iam::123456789012:root" }
@@ -327,21 +341,33 @@ def test_extract_actor_service(self):
327341 == "elasticloadbalancing.amazonaws.com"
328342 )
329343
330- def test_extract_actor_fallback_to_principal_id (self ):
331- user_identity = {"type" : "Unknown" , "principalId" : "AROAEXAMPLEID:session" }
332- assert (
333- CloudTrailTimeline ._extract_actor (user_identity ) == "AROAEXAMPLEID:session"
334- )
335-
336344 def test_extract_actor_unknown (self ):
337345 assert CloudTrailTimeline ._extract_actor ({}) == "Unknown"
338346
347+ def test_extract_actor_username_only_returns_unknown (self ):
348+ """When userIdentity carries only userName/principalId (no arn or
349+ invokedBy), we deliberately return "Unknown" — we rely on the ARN
350+ from the upstream service for the actor."""
351+ assert (
352+ CloudTrailTimeline ._extract_actor ({"type" : "IAMUser" , "userName" : "alice" })
353+ == "Unknown"
354+ )
355+ assert (
356+ CloudTrailTimeline ._extract_actor (
357+ {"type" : "Unknown" , "principalId" : "AROAEXAMPLEID:session" }
358+ )
359+ == "Unknown"
360+ )
361+
339362 def test_extract_actor_federated_user (self ):
340363 user_identity = {
341364 "type" : "FederatedUser" ,
342365 "arn" : "arn:aws:sts::123456789012:federated-user/developer" ,
343366 }
344- assert CloudTrailTimeline ._extract_actor (user_identity ) == "developer"
367+ assert (
368+ CloudTrailTimeline ._extract_actor (user_identity )
369+ == "federated-user/developer"
370+ )
345371
346372
347373class TestParseEvent :
@@ -380,7 +406,7 @@ def test_parse_event_success(self, mock_session, sample_cloudtrail_event):
380406 assert result is not None
381407 assert result ["event_name" ] == "RunInstances"
382408 assert result ["event_source" ] == "ec2.amazonaws.com"
383- assert result ["actor" ] == "admin"
409+ assert result ["actor" ] == "user/ admin"
384410 assert result ["actor_uid" ] == "arn:aws:iam::123456789012:user/admin"
385411 assert result ["actor_type" ] == "IAMUser"
386412
@@ -424,15 +450,18 @@ def test_parse_event_dict_cloud_trail_event(self, mock_session):
424450 "EventName" : "RunInstances" ,
425451 "EventSource" : "ec2.amazonaws.com" ,
426452 "CloudTrailEvent" : {
427- "userIdentity" : {"type" : "IAMUser" , "userName" : "admin" },
453+ "userIdentity" : {
454+ "type" : "IAMUser" ,
455+ "arn" : "arn:aws:iam::123456789012:user/admin" ,
456+ },
428457 },
429458 }
430459 timeline = CloudTrailTimeline (session = mock_session )
431460 result = timeline ._parse_event (event )
432461
433462 assert result is not None
434463 assert result ["event_name" ] == "RunInstances"
435- assert result ["actor" ] == "admin"
464+ assert result ["actor" ] == "user/ admin"
436465
437466 def test_parse_event_missing_event_id (self , mock_session ):
438467 """Test parsing event without EventId returns None (event_id is required)."""
@@ -506,7 +535,7 @@ def test_parse_event_missing_actor_type(self, mock_session):
506535
507536 assert result is not None
508537 assert result ["event_name" ] == "RunInstances"
509- assert result ["actor" ] == "admin"
538+ assert result ["actor" ] == "user/ admin"
510539 # actor_type should be None when not present in userIdentity
511540 assert result ["actor_type" ] is None
512541
0 commit comments