Skip to content

Commit 992e16e

Browse files
authored
fix: out of sequence audit logs for same timestamp logs (#63613)
1 parent 93b62f5 commit 992e16e

5 files changed

Lines changed: 173 additions & 1 deletion

File tree

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/**
2+
* Teleport
3+
* Copyright (C) 2026 Gravitational, Inc.
4+
*
5+
* This program is free software: you can redistribute it and/or modify
6+
* it under the terms of the GNU Affero General Public License as published by
7+
* the Free Software Foundation, either version 3 of the License, or
8+
* (at your option) any later version.
9+
*
10+
* This program is distributed in the hope that it will be useful,
11+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13+
* GNU Affero General Public License for more details.
14+
*
15+
* You should have received a copy of the GNU Affero General Public License
16+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
17+
*/
18+
19+
import { screen, within } from '@testing-library/react';
20+
21+
import { render } from 'design/utils/testing';
22+
23+
import { RawEvents } from '../../services/audit';
24+
import makeEvent from '../../services/audit/makeEvent';
25+
import EventList from './EventList';
26+
27+
describe('EventList', () => {
28+
it('should sort events with same timestamp by event index', () => {
29+
const sameTimestamp = '2025-09-08T21:25:49.265Z';
30+
31+
const mcpSessionStart = {
32+
code: 'TMCP001I',
33+
event: 'mcp.session.start',
34+
time: sameTimestamp,
35+
uid: '5dcf76ab-3f31-40a7-8550-bb29ecea1e42',
36+
user: 'admin',
37+
ei: 269750, // Lower event index - should be first
38+
sid: '5dcf76ab-3f31-40a7-8550-bb29ecea1e42',
39+
app_name: 'teleport-mcp-demo',
40+
} as RawEvents[typeof import('teleport/services/audit').eventCodes.MCP_SESSION_START];
41+
42+
const mcpSessionRequest = {
43+
code: 'TMCP003I',
44+
event: 'mcp.session.request',
45+
time: sameTimestamp,
46+
uid: 'int64:0',
47+
user: 'admin',
48+
ei: 667167, // Higher event index - should be second
49+
app_name: 'teleport-mcp-demo',
50+
message: {
51+
id: 'init64:0',
52+
jsonrpc: '2.0',
53+
method: 'initialize',
54+
params: {
55+
capabilities: {},
56+
clientInfo: {
57+
name: 'claude-ai',
58+
version: '0.1.0',
59+
},
60+
protocolVersion: '2025-06-18',
61+
},
62+
},
63+
} as RawEvents[typeof import('teleport/services/audit').eventCodes.MCP_SESSION_REQUEST];
64+
65+
const events = [makeEvent(mcpSessionStart), makeEvent(mcpSessionRequest)];
66+
67+
render(
68+
<EventList
69+
events={events}
70+
fetchMore={() => null}
71+
fetchStatus=""
72+
pageSize={50}
73+
/>
74+
);
75+
76+
const table = screen.getByRole('table');
77+
const rows = within(table).getAllByRole('row');
78+
79+
const firstDataRow = rows[1];
80+
const secondDataRow = rows[2];
81+
expect(
82+
within(firstDataRow).getByText(/MCP Session Started/i)
83+
).toBeInTheDocument();
84+
expect(
85+
within(secondDataRow).getByText(/MCP Session Request/i)
86+
).toBeInTheDocument();
87+
});
88+
89+
it('should handle events with different timestamps correctly', () => {
90+
const olderEvent = {
91+
code: 'TMCP001I',
92+
event: 'mcp.session.start',
93+
time: '2025-09-08T21:25:48.000Z',
94+
uid: 'uid-1',
95+
user: 'admin',
96+
ei: 999999,
97+
sid: 'sid-1',
98+
app_name: 'teleport-mcp-demo',
99+
} as RawEvents[typeof import('teleport/services/audit').eventCodes.MCP_SESSION_START];
100+
101+
const newerEvent = {
102+
code: 'TMCP003I',
103+
event: 'mcp.session.request',
104+
time: '2025-09-08T21:25:49.000Z',
105+
uid: 'uid-2',
106+
user: 'admin',
107+
ei: 1,
108+
app_name: 'teleport-mcp-demo',
109+
message: {
110+
id: 'int64:0',
111+
jsonrpc: '2.0',
112+
method: 'initialize',
113+
params: {
114+
capabilities: {},
115+
clientInfo: {
116+
name: 'claude-ai',
117+
version: '0.1.0',
118+
},
119+
protocolVersion: '2025-06-18',
120+
},
121+
},
122+
} as RawEvents[typeof import('teleport/services/audit').eventCodes.MCP_SESSION_REQUEST];
123+
124+
const events = [makeEvent(olderEvent), makeEvent(newerEvent)];
125+
126+
render(
127+
<EventList
128+
events={events}
129+
fetchMore={() => null}
130+
fetchStatus=""
131+
pageSize={50}
132+
/>
133+
);
134+
135+
const table = screen.getByRole('table');
136+
const rows = within(table).getAllByRole('row');
137+
138+
const firstDataRow = rows[1];
139+
const secondDataRow = rows[2];
140+
141+
expect(
142+
within(firstDataRow).getByText(/MCP Session Request/i)
143+
).toBeInTheDocument();
144+
expect(
145+
within(secondDataRow).getByText(/MCP Session Started/i)
146+
).toBeInTheDocument();
147+
});
148+
});

web/packages/teleport/src/Audit/EventList/EventList.tsx

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ export default function EventList(props: Props) {
5353
headerText: 'Created (UTC)',
5454
isSortable: true,
5555
render: renderTimeCell,
56+
onSort: onSortTime,
5657
},
5758
{
5859
altKey: 'show-details-btn',
@@ -63,7 +64,10 @@ export default function EventList(props: Props) {
6364
isSearchable
6465
searchableProps={['code', 'codeDesc', 'time', 'user', 'message', 'id']}
6566
customSearchMatchers={[dateTimeMatcher(['time'])]}
66-
initialSort={{ key: 'time', dir: 'DESC' }}
67+
initialSort={{
68+
key: 'time',
69+
dir: 'DESC',
70+
}}
6771
pagination={{ pageSize }}
6872
fetching={{
6973
onFetchMore: fetchMore,
@@ -106,6 +110,22 @@ export function renderDescCell({ message }: Event) {
106110
return <Cell style={{ wordBreak: 'break-word' }}>{message}</Cell>;
107111
}
108112

113+
const onSortTime = (a: Event, b: Event) => {
114+
const aTime = a.time.getTime();
115+
const bTime = b.time.getTime();
116+
117+
const timeDiff = aTime - bTime;
118+
if (timeDiff !== 0) {
119+
return timeDiff;
120+
}
121+
122+
// If times are equal, sort by event index (ei)
123+
const aEventIndex = a.eventIndex || 0;
124+
const bEventIndex = b.eventIndex || 0;
125+
// HIGHER index - lower index to counteract the reversal for desc sort
126+
return bEventIndex - aEventIndex;
127+
};
128+
109129
type Props = {
110130
events: State['events'];
111131
fetchMore: State['fetchMore'];

web/packages/teleport/src/services/audit/audit.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ test('fetch events', async () => {
4646
code: 'T6000I',
4747
user: '90678c66-ffcc-4f02.im-a-cluster-name',
4848
time: new Date('2021-05-25T07:34:22.204Z'),
49+
eventIndex: 0,
4950
raw: {
5051
cluster_name: 'im-a-cluster-name',
5152
code: 'T6000I',
@@ -67,6 +68,7 @@ test('fetch events', async () => {
6768
code: 'T1000I',
6869
user: 'root',
6970
time: new Date('2021-05-25T14:37:27.848Z'),
71+
eventIndex: 0,
7072
raw: {
7173
cluster_name: 'im-a-cluster-name',
7274
code: 'T1000I',

web/packages/teleport/src/services/audit/makeEvent.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2558,6 +2558,7 @@ export default function makeEvent(json: any): Event {
25582558
message: formatter.format(json as any),
25592559
id: getId(json),
25602560
code: json.code,
2561+
eventIndex: json.ei,
25612562
user: json.user,
25622563
time: new Date(json.time),
25632564
raw: json,

web/packages/teleport/src/services/audit/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2471,6 +2471,7 @@ export type Events = {
24712471
code: key;
24722472
codeDesc: string;
24732473
raw: RawEvents[key];
2474+
eventIndex: number;
24742475
};
24752476
};
24762477

0 commit comments

Comments
 (0)