From 1f0d51bb8a0039825d5913c4012a1a3fa515cce9 Mon Sep 17 00:00:00 2001 From: Kevin Heis Date: Tue, 15 Jul 2025 18:23:11 -0700 Subject: [PATCH] Add category notes functionality to audit log events (#56268) --- src/audit-logs/components/GroupedEvents.tsx | 8 ++- src/audit-logs/lib/config.json | 8 ++- src/audit-logs/lib/index.ts | 9 +++ src/audit-logs/pages/audit-log-events.tsx | 10 ++- src/audit-logs/tests/rendering.ts | 45 ++++++++++++ src/audit-logs/tests/unit/category-notes.ts | 76 +++++++++++++++++++++ src/audit-logs/types.ts | 8 +++ 7 files changed, 160 insertions(+), 4 deletions(-) create mode 100644 src/audit-logs/tests/unit/category-notes.ts diff --git a/src/audit-logs/components/GroupedEvents.tsx b/src/audit-logs/components/GroupedEvents.tsx index 270d9dd5b2..d5c161b03f 100644 --- a/src/audit-logs/components/GroupedEvents.tsx +++ b/src/audit-logs/components/GroupedEvents.tsx @@ -7,9 +7,10 @@ import type { AuditLogEventT } from '../types' type Props = { auditLogEvents: AuditLogEventT[] 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 eventSlug = slug(category) @@ -39,6 +40,11 @@ export default function GroupedEvents({ auditLogEvents, category }: Props) { {category} + {categoryNote && ( +
+

{categoryNote}

+
+ )}
{auditLogEvents.map((event) => (
diff --git a/src/audit-logs/lib/config.json b/src/audit-logs/lib/config.json index ae2cdc8b31..a4aed9448c 100644 --- a/src/audit-logs/lib/config.json +++ b/src/audit-logs/lib/config.json @@ -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.", "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" -} \ No newline at end of file +} diff --git a/src/audit-logs/lib/index.ts b/src/audit-logs/lib/index.ts index 8321e7a845..9f1608e309 100644 --- a/src/audit-logs/lib/index.ts +++ b/src/audit-logs/lib/index.ts @@ -8,7 +8,10 @@ import type { CategorizedEvents, VersionedAuditLogData, RawAuditLogEventT, + CategoryNotes, + AuditLogConfig, } from '../types' +import config from './config.json' export const AUDIT_LOG_DATA_DIR = 'src/audit-logs/data' @@ -21,6 +24,12 @@ type PipelineConfig = { appendedDescriptions: Record } +// get category notes from config +export function getCategoryNotes(): CategoryNotes { + const auditLogConfig = config as AuditLogConfig + return auditLogConfig.categoryNotes || {} +} + type TitleResolutionContext = { pages: Record redirects: Record diff --git a/src/audit-logs/pages/audit-log-events.tsx b/src/audit-logs/pages/audit-log-events.tsx index d5cf0c4802..ac114c6c37 100644 --- a/src/audit-logs/pages/audit-log-events.tsx +++ b/src/audit-logs/pages/audit-log-events.tsx @@ -14,18 +14,20 @@ import { import { AutomatedPage } from '@/automated-pipelines/components/AutomatedPage' import { HeadingLink } from '@/frame/components/article/HeadingLink' import GroupedEvents from '../components/GroupedEvents' -import type { CategorizedEvents } from '../types' +import type { CategorizedEvents, CategoryNotes } from '../types' type Props = { mainContext: MainContextT automatedPageContext: AutomatedPageContextT auditLogEvents: CategorizedEvents + categoryNotes: CategoryNotes } export default function AuditLogEvents({ mainContext, automatedPageContext, auditLogEvents, + categoryNotes, }: Props) { const content = ( <> @@ -38,6 +40,7 @@ export default function AuditLogEvents({ key={category} category={category} auditLogEvents={auditLogEvents[category]} + categoryNote={categoryNotes[category]} /> ) })} @@ -55,7 +58,7 @@ export default function AuditLogEvents({ export const getServerSideProps: GetServerSideProps = async (context) => { 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 res = context.res as object @@ -77,6 +80,8 @@ export const getServerSideProps: GetServerSideProps = async (context) => auditLogEvents = getCategorizedAuditLogEvents('organization', currentVersion) } + const categoryNotes = getCategoryNotes() + const auditLogEventsMiniTocs = await getAutomatedPageMiniTocItems( Object.keys(auditLogEvents).map((category) => category), context, @@ -86,6 +91,7 @@ export const getServerSideProps: GetServerSideProps = async (context) => return { props: { auditLogEvents, + categoryNotes, mainContext, automatedPageContext: getAutomatedPageContextFromRequest(req), }, diff --git a/src/audit-logs/tests/rendering.ts b/src/audit-logs/tests/rendering.ts index 349f4c124a..c1e4e51db4 100644 --- a/src/audit-logs/tests/rendering.ts +++ b/src/audit-logs/tests/rendering.ts @@ -98,4 +98,49 @@ describe('audit log events docs', () => { const $lead = $(leadSelector) 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) + } + } + }) }) diff --git a/src/audit-logs/tests/unit/category-notes.ts b/src/audit-logs/tests/unit/category-notes.ts new file mode 100644 index 0000000000..2d4e31638b --- /dev/null +++ b/src/audit-logs/tests/unit/category-notes.ts @@ -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), + }), + ) + } + }) +}) diff --git a/src/audit-logs/types.ts b/src/audit-logs/types.ts index c7e20efbde..c532b6ad37 100644 --- a/src/audit-logs/types.ts +++ b/src/audit-logs/types.ts @@ -1,5 +1,7 @@ export type CategorizedEvents = Record +export type CategoryNotes = Record + export type AuditLogEventT = { action: string description: string @@ -19,3 +21,9 @@ export type RawAuditLogEventT = { } export type VersionedAuditLogData = Record> + +export type AuditLogConfig = { + sha: string + appendedDescriptions: Record + categoryNotes?: CategoryNotes +}