audit logs

This commit is contained in:
2025-08-25 21:42:41 -04:00
parent 19364fcc76
commit b39da8d233
4 changed files with 477 additions and 160 deletions

View File

@ -1,4 +1,4 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import {
Table,
Card,
@ -28,6 +28,7 @@ import {
} from '@ant-design/icons';
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import { apiService, AuditEvent, AuditQueryParams } from '../services/apiService';
dayjs.extend(relativeTime);
@ -35,103 +36,10 @@ const { Title, Text } = Typography;
const { RangePicker } = DatePicker;
const { Option } = Select;
interface AuditLogEntry {
id: string;
timestamp: string;
user_id: string;
action: string;
resource_type: string;
resource_id: string;
status: 'success' | 'failure' | 'warning';
ip_address: string;
user_agent: string;
details: Record<string, any>;
}
// Mock audit data for demonstration
const mockAuditData: AuditLogEntry[] = [
{
id: '1',
timestamp: dayjs().subtract(1, 'hour').toISOString(),
user_id: 'admin@example.com',
action: 'CREATE_APPLICATION',
resource_type: 'application',
resource_id: 'com.example.newapp',
status: 'success',
ip_address: '192.168.1.100',
user_agent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
details: {
app_link: 'https://newapp.example.com',
owner: 'Development Team'
}
},
{
id: '2',
timestamp: dayjs().subtract(2, 'hours').toISOString(),
user_id: 'user@example.com',
action: 'CREATE_TOKEN',
resource_type: 'token',
resource_id: 'token-abc123',
status: 'success',
ip_address: '192.168.1.101',
user_agent: 'curl/7.68.0',
details: {
app_id: 'com.example.app',
permissions: ['repo.read', 'repo.write']
}
},
{
id: '3',
timestamp: dayjs().subtract(3, 'hours').toISOString(),
user_id: 'admin@example.com',
action: 'DELETE_TOKEN',
resource_type: 'token',
resource_id: 'token-xyz789',
status: 'success',
ip_address: '192.168.1.100',
user_agent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
details: {
app_id: 'com.example.oldapp',
reason: 'Token compromised'
}
},
{
id: '4',
timestamp: dayjs().subtract(4, 'hours').toISOString(),
user_id: 'user@example.com',
action: 'VERIFY_TOKEN',
resource_type: 'token',
resource_id: 'token-def456',
status: 'failure',
ip_address: '192.168.1.102',
user_agent: 'PostmanRuntime/7.28.4',
details: {
app_id: 'com.example.app',
error: 'Token expired'
}
},
{
id: '5',
timestamp: dayjs().subtract(6, 'hours').toISOString(),
user_id: 'admin@example.com',
action: 'UPDATE_APPLICATION',
resource_type: 'application',
resource_id: 'com.example.app',
status: 'success',
ip_address: '192.168.1.100',
user_agent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
details: {
changes: {
callback_url: 'https://updated.example.com/callback'
}
}
},
];
const Audit: React.FC = () => {
const [auditData, setAuditData] = useState<AuditLogEntry[]>(mockAuditData);
const [filteredData, setFilteredData] = useState<AuditLogEntry[]>(mockAuditData);
const [loading, setLoading] = useState(false);
const [auditData, setAuditData] = useState<AuditEvent[]>([]);
const [filteredData, setFilteredData] = useState<AuditEvent[]>([]);
const [loading, setLoading] = useState(true);
const [filters, setFilters] = useState({
dateRange: null as any,
action: '',
@ -140,36 +48,70 @@ const Audit: React.FC = () => {
resourceType: '',
});
const applyFilters = () => {
let filtered = [...auditData];
// Load audit data on component mount
useEffect(() => {
loadAuditData();
}, []);
if (filters.dateRange && filters.dateRange.length === 2) {
const [start, end] = filters.dateRange;
filtered = filtered.filter(entry => {
const entryDate = dayjs(entry.timestamp);
return entryDate.isAfter(start) && entryDate.isBefore(end);
const loadAuditData = async () => {
try {
setLoading(true);
const response = await apiService.getAuditEvents({
limit: 100,
order_by: 'timestamp',
order_desc: true,
});
setAuditData(response.events);
setFilteredData(response.events);
} catch (error) {
console.error('Failed to load audit data:', error);
// Keep empty arrays on error
setAuditData([]);
setFilteredData([]);
} finally {
setLoading(false);
}
};
if (filters.action) {
filtered = filtered.filter(entry => entry.action === filters.action);
const applyFilters = async () => {
// For real-time filtering, we'll use the API with filters
try {
setLoading(true);
const params: AuditQueryParams = {
limit: 100,
order_by: 'timestamp',
order_desc: true,
};
if (filters.dateRange && filters.dateRange.length === 2) {
const [start, end] = filters.dateRange;
params.start_time = start.toISOString();
params.end_time = end.toISOString();
}
if (filters.action) {
params.event_types = [filters.action];
}
if (filters.status) {
params.statuses = [filters.status];
}
if (filters.user) {
params.actor_id = filters.user;
}
if (filters.resourceType) {
params.resource_type = filters.resourceType;
}
const response = await apiService.getAuditEvents(params);
setFilteredData(response.events);
} catch (error) {
console.error('Failed to apply filters:', error);
} finally {
setLoading(false);
}
if (filters.status) {
filtered = filtered.filter(entry => entry.status === filters.status);
}
if (filters.user) {
filtered = filtered.filter(entry =>
entry.user_id.toLowerCase().includes(filters.user.toLowerCase())
);
}
if (filters.resourceType) {
filtered = filtered.filter(entry => entry.resource_type === filters.resourceType);
}
setFilteredData(filtered);
};
const clearFilters = () => {
@ -180,7 +122,7 @@ const Audit: React.FC = () => {
user: '',
resourceType: '',
});
setFilteredData(auditData);
loadAuditData(); // Reload original data
};
const getStatusIcon = (status: string) => {
@ -216,42 +158,42 @@ const Audit: React.FC = () => {
</Text>
</div>
),
sorter: (a: AuditLogEntry, b: AuditLogEntry) =>
sorter: (a: AuditEvent, b: AuditEvent) =>
dayjs(a.timestamp).unix() - dayjs(b.timestamp).unix(),
defaultSortOrder: 'descend' as const,
},
{
title: 'User',
dataIndex: 'user_id',
key: 'user_id',
render: (userId: string) => (
dataIndex: 'actor_id',
key: 'actor_id',
render: (actorId: string) => (
<div>
<UserOutlined style={{ marginRight: '8px' }} />
{userId}
{actorId || 'System'}
</div>
),
},
{
title: 'Action',
dataIndex: 'action',
key: 'action',
render: (action: string) => (
dataIndex: 'type',
key: 'type',
render: (type: string) => (
<div>
{getActionIcon(action)}
<span style={{ marginLeft: '8px' }}>{action.replace(/_/g, ' ')}</span>
{getActionIcon(type)}
<span style={{ marginLeft: '8px' }}>{type.replace(/_/g, ' ').replace(/\./g, ' ')}</span>
</div>
),
},
{
title: 'Resource',
key: 'resource',
render: (_: any, record: AuditLogEntry) => (
render: (_: any, record: AuditEvent) => (
<div>
<div>
<Tag color="blue">{record.resource_type.toUpperCase()}</Tag>
<Tag color="blue">{record.resource_type?.toUpperCase() || 'N/A'}</Tag>
</div>
<Text type="secondary" style={{ fontSize: '12px' }}>
{record.resource_id}
{record.resource_id || 'N/A'}
</Text>
</div>
),
@ -271,13 +213,13 @@ const Audit: React.FC = () => {
},
{
title: 'IP Address',
dataIndex: 'ip_address',
key: 'ip_address',
render: (ip: string) => <Text code>{ip}</Text>,
dataIndex: 'actor_ip',
key: 'actor_ip',
render: (ip: string) => <Text code>{ip || 'N/A'}</Text>,
},
];
const expandedRowRender = (record: AuditLogEntry) => (
const expandedRowRender = (record: AuditEvent) => (
<Card size="small" title="Event Details">
<Row gutter={16}>
<Col span={12}>
@ -360,7 +302,7 @@ const Audit: React.FC = () => {
<div style={{ textAlign: 'center' }}>
<UserOutlined style={{ fontSize: '32px', color: '#722ed1', marginBottom: '8px' }} />
<div style={{ fontSize: '24px', fontWeight: 'bold' }}>
{new Set(filteredData.map(e => e.user_id)).size}
{new Set(filteredData.map(e => e.actor_id).filter(id => id)).size}
</div>
<div>Unique Users</div>
</div>
@ -401,12 +343,14 @@ const Audit: React.FC = () => {
onChange={(value) => setFilters({ ...filters, action: value })}
allowClear
>
<Option value="CREATE_APPLICATION">Create Application</Option>
<Option value="UPDATE_APPLICATION">Update Application</Option>
<Option value="DELETE_APPLICATION">Delete Application</Option>
<Option value="CREATE_TOKEN">Create Token</Option>
<Option value="DELETE_TOKEN">Delete Token</Option>
<Option value="VERIFY_TOKEN">Verify Token</Option>
<Option value="app.created">Application Created</Option>
<Option value="app.updated">Application Updated</Option>
<Option value="app.deleted">Application Deleted</Option>
<Option value="auth.token_created">Token Created</Option>
<Option value="auth.token_revoked">Token Revoked</Option>
<Option value="auth.token_validated">Token Validated</Option>
<Option value="auth.login">Login</Option>
<Option value="auth.login_failed">Login Failed</Option>
</Select>
</Col>
<Col span={4}>
@ -465,16 +409,16 @@ const Audit: React.FC = () => {
color={entry.status === 'success' ? 'green' : entry.status === 'failure' ? 'red' : 'orange'}
>
<div>
<Text strong>{entry.action.replace(/_/g, ' ')}</Text>
<Text strong>{entry.type.replace(/_/g, ' ').replace(/\./g, ' ')}</Text>
<div>
<Text type="secondary">
{entry.user_id} {dayjs(entry.timestamp).fromNow()}
{entry.actor_id || 'System'} {dayjs(entry.timestamp).fromNow()}
</Text>
</div>
<div>
<Tag>{entry.resource_type}</Tag>
<Tag>{entry.resource_type || 'N/A'}</Tag>
<Text type="secondary" style={{ marginLeft: '8px' }}>
{entry.resource_id}
{entry.resource_id || 'N/A'}
</Text>
</div>
</div>
@ -485,13 +429,15 @@ const Audit: React.FC = () => {
{/* Audit Log Table */}
<Card title="Audit Log Entries">
<Alert
message="Demo Data"
description="This audit log shows simulated data for demonstration purposes. In production, this would display real audit events from your KMS system."
type="info"
showIcon
style={{ marginBottom: '16px' }}
/>
{filteredData.length === 0 && !loading && (
<Alert
message="No Audit Events Found"
description="No audit events match your current filters. Try adjusting the filters or check if any events have been logged to the system."
type="info"
showIcon
style={{ marginBottom: '16px' }}
/>
)}
<Table
columns={columns}