Add category notes functionality to audit log events (#56268)
This commit is contained in:
@@ -7,9 +7,10 @@ import type { AuditLogEventT } from '../types'
|
|||||||
type Props = {
|
type Props = {
|
||||||
auditLogEvents: AuditLogEventT[]
|
auditLogEvents: AuditLogEventT[]
|
||||||
category: string
|
category: string
|
||||||
|
categoryNote?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function GroupedEvents({ auditLogEvents, category }: Props) {
|
export default function GroupedEvents({ auditLogEvents, category, categoryNote }: Props) {
|
||||||
const { t } = useTranslation('audit_logs')
|
const { t } = useTranslation('audit_logs')
|
||||||
const eventSlug = slug(category)
|
const eventSlug = slug(category)
|
||||||
|
|
||||||
@@ -39,6 +40,11 @@ export default function GroupedEvents({ auditLogEvents, category }: Props) {
|
|||||||
<HeadingLink as="h3" slug={eventSlug}>
|
<HeadingLink as="h3" slug={eventSlug}>
|
||||||
{category}
|
{category}
|
||||||
</HeadingLink>
|
</HeadingLink>
|
||||||
|
{categoryNote && (
|
||||||
|
<div className="category-note mb-3 p-3 color-border-default border rounded-2">
|
||||||
|
<p className="mb-0">{categoryNote}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div>
|
<div>
|
||||||
{auditLogEvents.map((event) => (
|
{auditLogEvents.map((event) => (
|
||||||
<div key={event.action} style={{ marginBottom: '3rem' }}>
|
<div key={event.action} style={{ marginBottom: '3rem' }}>
|
||||||
|
|||||||
@@ -3,5 +3,11 @@
|
|||||||
"apiOnlyEvents": "This event is not available in the web interface, only via the REST API, audit log streaming, or JSON/CSV exports.",
|
"apiOnlyEvents": "This event is not available in the web interface, only via the REST API, audit log streaming, or JSON/CSV exports.",
|
||||||
"apiRequestEvent": "This event is only available via audit log streaming."
|
"apiRequestEvent": "This event is only available via audit log streaming."
|
||||||
},
|
},
|
||||||
|
"_categoryNotesDocumentation": "Category notes provide additional context for audit log event categories. Currently, notes are plain text and not version-specific. Future enhancement: add versioning support for different deployment types (GHEC, GHES, FPT).",
|
||||||
|
"categoryNotes": {
|
||||||
|
"members_can_create_pages": "For more information, see \"Managing the publication of GitHub Pages sites for your organization.\"",
|
||||||
|
"git": "Note: Git events have special access requirements and retention policies that differ from other audit log events. For GitHub Enterprise Cloud, access Git events via the REST API only with 7-day retention. For GitHub Enterprise Server, Git events must be enabled in audit log configuration and are not included in search results.",
|
||||||
|
"sso_redirect": "Note: Automatically redirecting users to sign in is currently in beta for Enterprise Managed Users and subject to change."
|
||||||
|
},
|
||||||
"sha": "30f9be27cbe4d9f3729f8fb335ce8b254ca3b54a"
|
"sha": "30f9be27cbe4d9f3729f8fb335ce8b254ca3b54a"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,10 @@ import type {
|
|||||||
CategorizedEvents,
|
CategorizedEvents,
|
||||||
VersionedAuditLogData,
|
VersionedAuditLogData,
|
||||||
RawAuditLogEventT,
|
RawAuditLogEventT,
|
||||||
|
CategoryNotes,
|
||||||
|
AuditLogConfig,
|
||||||
} from '../types'
|
} from '../types'
|
||||||
|
import config from './config.json'
|
||||||
|
|
||||||
export const AUDIT_LOG_DATA_DIR = 'src/audit-logs/data'
|
export const AUDIT_LOG_DATA_DIR = 'src/audit-logs/data'
|
||||||
|
|
||||||
@@ -21,6 +24,12 @@ type PipelineConfig = {
|
|||||||
appendedDescriptions: Record<string, string>
|
appendedDescriptions: Record<string, string>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// get category notes from config
|
||||||
|
export function getCategoryNotes(): CategoryNotes {
|
||||||
|
const auditLogConfig = config as AuditLogConfig
|
||||||
|
return auditLogConfig.categoryNotes || {}
|
||||||
|
}
|
||||||
|
|
||||||
type TitleResolutionContext = {
|
type TitleResolutionContext = {
|
||||||
pages: Record<string, any>
|
pages: Record<string, any>
|
||||||
redirects: Record<string, string>
|
redirects: Record<string, string>
|
||||||
|
|||||||
@@ -14,18 +14,20 @@ import {
|
|||||||
import { AutomatedPage } from '@/automated-pipelines/components/AutomatedPage'
|
import { AutomatedPage } from '@/automated-pipelines/components/AutomatedPage'
|
||||||
import { HeadingLink } from '@/frame/components/article/HeadingLink'
|
import { HeadingLink } from '@/frame/components/article/HeadingLink'
|
||||||
import GroupedEvents from '../components/GroupedEvents'
|
import GroupedEvents from '../components/GroupedEvents'
|
||||||
import type { CategorizedEvents } from '../types'
|
import type { CategorizedEvents, CategoryNotes } from '../types'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
mainContext: MainContextT
|
mainContext: MainContextT
|
||||||
automatedPageContext: AutomatedPageContextT
|
automatedPageContext: AutomatedPageContextT
|
||||||
auditLogEvents: CategorizedEvents
|
auditLogEvents: CategorizedEvents
|
||||||
|
categoryNotes: CategoryNotes
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function AuditLogEvents({
|
export default function AuditLogEvents({
|
||||||
mainContext,
|
mainContext,
|
||||||
automatedPageContext,
|
automatedPageContext,
|
||||||
auditLogEvents,
|
auditLogEvents,
|
||||||
|
categoryNotes,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const content = (
|
const content = (
|
||||||
<>
|
<>
|
||||||
@@ -38,6 +40,7 @@ export default function AuditLogEvents({
|
|||||||
key={category}
|
key={category}
|
||||||
category={category}
|
category={category}
|
||||||
auditLogEvents={auditLogEvents[category]}
|
auditLogEvents={auditLogEvents[category]}
|
||||||
|
categoryNote={categoryNotes[category]}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
@@ -55,7 +58,7 @@ export default function AuditLogEvents({
|
|||||||
|
|
||||||
export const getServerSideProps: GetServerSideProps<Props> = async (context) => {
|
export const getServerSideProps: GetServerSideProps<Props> = async (context) => {
|
||||||
const { getAutomatedPageMiniTocItems } = await import('@/frame/lib/get-mini-toc-items')
|
const { getAutomatedPageMiniTocItems } = await import('@/frame/lib/get-mini-toc-items')
|
||||||
const { getCategorizedAuditLogEvents } = await import('../lib')
|
const { getCategorizedAuditLogEvents, getCategoryNotes } = await import('../lib')
|
||||||
|
|
||||||
const req = context.req as object
|
const req = context.req as object
|
||||||
const res = context.res as object
|
const res = context.res as object
|
||||||
@@ -77,6 +80,8 @@ export const getServerSideProps: GetServerSideProps<Props> = async (context) =>
|
|||||||
auditLogEvents = getCategorizedAuditLogEvents('organization', currentVersion)
|
auditLogEvents = getCategorizedAuditLogEvents('organization', currentVersion)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const categoryNotes = getCategoryNotes()
|
||||||
|
|
||||||
const auditLogEventsMiniTocs = await getAutomatedPageMiniTocItems(
|
const auditLogEventsMiniTocs = await getAutomatedPageMiniTocItems(
|
||||||
Object.keys(auditLogEvents).map((category) => category),
|
Object.keys(auditLogEvents).map((category) => category),
|
||||||
context,
|
context,
|
||||||
@@ -86,6 +91,7 @@ export const getServerSideProps: GetServerSideProps<Props> = async (context) =>
|
|||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
auditLogEvents,
|
auditLogEvents,
|
||||||
|
categoryNotes,
|
||||||
mainContext,
|
mainContext,
|
||||||
automatedPageContext: getAutomatedPageContextFromRequest(req),
|
automatedPageContext: getAutomatedPageContextFromRequest(req),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -98,4 +98,49 @@ describe('audit log events docs', () => {
|
|||||||
const $lead = $(leadSelector)
|
const $lead = $(leadSelector)
|
||||||
expect($lead.length).toBe(1)
|
expect($lead.length).toBe(1)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('category notes are rendered when present', async () => {
|
||||||
|
// Test organization page which should have category notes
|
||||||
|
const $ = await getDOM(
|
||||||
|
'/organizations/keeping-your-organization-secure/managing-security-settings-for-your-organization/audit-log-events-for-your-organization',
|
||||||
|
)
|
||||||
|
|
||||||
|
// Look for category note elements - they should appear before tables
|
||||||
|
const categoryNotes = $('.category-note')
|
||||||
|
|
||||||
|
// If there are categories with notes configured, we should see them rendered
|
||||||
|
if (categoryNotes.length > 0) {
|
||||||
|
categoryNotes.each((_, note) => {
|
||||||
|
const $note = $(note)
|
||||||
|
expect($note.text().length).toBeGreaterThan(0)
|
||||||
|
|
||||||
|
// Should be followed by a div (the category events)
|
||||||
|
const $nextDiv = $note.next('div')
|
||||||
|
expect($nextDiv.length).toBe(1)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('git category note is rendered on appropriate pages', async () => {
|
||||||
|
// Test enterprise page which should have git category note for GHES
|
||||||
|
const $ = await getDOM(
|
||||||
|
'/enterprise-server@latest/admin/monitoring-activity-in-your-enterprise/reviewing-audit-logs-for-your-enterprise/audit-log-events-for-your-enterprise',
|
||||||
|
)
|
||||||
|
|
||||||
|
// Look for git category heading
|
||||||
|
const gitHeading = $('#git')
|
||||||
|
if (gitHeading.length > 0) {
|
||||||
|
// Should have a category note before the div
|
||||||
|
const $noteOrTable = gitHeading.next()
|
||||||
|
|
||||||
|
// Either the next element is a note (followed by div) or directly a div
|
||||||
|
if ($noteOrTable.hasClass('category-note')) {
|
||||||
|
expect($noteOrTable.text()).toContain('Git events')
|
||||||
|
expect($noteOrTable.next('div').length).toBe(1)
|
||||||
|
} else if ($noteOrTable.is('div')) {
|
||||||
|
// Direct div is fine too - means no note for this category
|
||||||
|
expect($noteOrTable.is('div')).toBe(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
76
src/audit-logs/tests/unit/category-notes.ts
Normal file
76
src/audit-logs/tests/unit/category-notes.ts
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { describe, expect, test } from 'vitest'
|
||||||
|
|
||||||
|
import { getCategorizedAuditLogEvents } from '../../lib'
|
||||||
|
import config from '../../lib/config.json'
|
||||||
|
|
||||||
|
describe('audit log category notes', () => {
|
||||||
|
test('config contains expected category notes', () => {
|
||||||
|
expect(config.categoryNotes).toBeDefined()
|
||||||
|
expect(typeof config.categoryNotes).toBe('object')
|
||||||
|
|
||||||
|
// Check that we have the specific category notes mentioned in the issue
|
||||||
|
expect(config.categoryNotes).toHaveProperty('members_can_create_pages')
|
||||||
|
expect(config.categoryNotes).toHaveProperty('git')
|
||||||
|
expect(config.categoryNotes).toHaveProperty('sso_redirect')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('category notes are strings', () => {
|
||||||
|
if (config.categoryNotes) {
|
||||||
|
Object.values(config.categoryNotes).forEach((note) => {
|
||||||
|
expect(typeof note).toBe('string')
|
||||||
|
expect(note.length).toBeGreaterThan(0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('members_can_create_pages note contains reference to GitHub Pages', () => {
|
||||||
|
const note = config.categoryNotes?.['members_can_create_pages']
|
||||||
|
expect(note).toContain('GitHub Pages')
|
||||||
|
expect(note).toContain('organization')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('git category note contains REST API information', () => {
|
||||||
|
const note = config.categoryNotes?.['git']
|
||||||
|
expect(note).toContain('REST API')
|
||||||
|
expect(note).toContain('7-day retention')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('sso_redirect note mentions beta status', () => {
|
||||||
|
const note = config.categoryNotes?.['sso_redirect']
|
||||||
|
expect(note).toContain('beta')
|
||||||
|
expect(note).toContain('Enterprise Managed Users')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('category notes do not interfere with event categorization', () => {
|
||||||
|
// Test that adding category notes doesn't break existing functionality
|
||||||
|
const organizationEvents = getCategorizedAuditLogEvents('organization', 'free-pro-team@latest')
|
||||||
|
const enterpriseEvents = getCategorizedAuditLogEvents('enterprise', 'enterprise-cloud@latest')
|
||||||
|
|
||||||
|
// Should still have categorized events
|
||||||
|
expect(Object.keys(organizationEvents).length).toBeGreaterThan(0)
|
||||||
|
expect(Object.keys(enterpriseEvents).length).toBeGreaterThan(0)
|
||||||
|
|
||||||
|
// Each category should still contain arrays of events
|
||||||
|
Object.values(organizationEvents).forEach((events) => {
|
||||||
|
expect(Array.isArray(events)).toBe(true)
|
||||||
|
if (events.length > 0) {
|
||||||
|
expect(events[0]).toHaveProperty('action')
|
||||||
|
expect(events[0]).toHaveProperty('description')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
test('category notes are properly typed', () => {
|
||||||
|
// This test will pass once we update the types
|
||||||
|
const notes = config.categoryNotes
|
||||||
|
if (notes) {
|
||||||
|
expect(notes).toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
members_can_create_pages: expect.any(String),
|
||||||
|
git: expect.any(String),
|
||||||
|
sso_redirect: expect.any(String),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
export type CategorizedEvents = Record<string, AuditLogEventT[]>
|
export type CategorizedEvents = Record<string, AuditLogEventT[]>
|
||||||
|
|
||||||
|
export type CategoryNotes = Record<string, string>
|
||||||
|
|
||||||
export type AuditLogEventT = {
|
export type AuditLogEventT = {
|
||||||
action: string
|
action: string
|
||||||
description: string
|
description: string
|
||||||
@@ -19,3 +21,9 @@ export type RawAuditLogEventT = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type VersionedAuditLogData = Record<string, Record<string, AuditLogEventT[]>>
|
export type VersionedAuditLogData = Record<string, Record<string, AuditLogEventT[]>>
|
||||||
|
|
||||||
|
export type AuditLogConfig = {
|
||||||
|
sha: string
|
||||||
|
appendedDescriptions: Record<string, string>
|
||||||
|
categoryNotes?: CategoryNotes
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user