1
0
mirror of synced 2025-12-19 09:57:42 -05:00

feat: add AuditLogsTransformer for Article API (#58754)

This commit is contained in:
Kevin Heis
2025-12-11 12:17:32 -08:00
committed by GitHub
parent 50f54eb7a6
commit babee40375
17 changed files with 538 additions and 11 deletions

View File

@@ -0,0 +1,30 @@
# {{ page.title }}
{{ page.intro }}
{{ manualContent }}
## Audit log events
{% for categoryEntry in categorizedEvents %}
{% assign categoryName = categoryEntry[0] %}
{% assign events = categoryEntry[1] %}
### {{ categoryName }}
{% if categoryNotes[categoryName] %}
{{ categoryNotes[categoryName] }}
{% endif %}
{% for event in events %}
#### `{{ event.action }}`
{{ event.description }}
**Fields:** {% if event.fields %}{% for field in event.fields %}`{{ field }}`{% unless forloop.last %}, {% endunless %}{% endfor %}{% else %}No fields available{% endif %}
{% if event.docs_reference_links and event.docs_reference_links != 'N/A' %}
**Reference:** {{ event.docs_reference_links }}
{% endif %}
{% endfor %}
{% endfor %}

View File

@@ -0,0 +1,95 @@
import { beforeAll, describe, expect, test } from 'vitest'
import { get } from '@/tests/helpers/e2etest'
const makeURL = (pathname: string): string => {
const params = new URLSearchParams({ pathname })
return `/api/article/body?${params}`
}
describe('Audit Logs transformer', () => {
beforeAll(() => {
if (!process.env.ROOT) {
console.warn(
'WARNING: The Audit Logs transformer tests require the ROOT environment variable to be set to the fixture root',
)
}
})
test('Security log events page renders with markdown structure', async () => {
const res = await get(
makeURL('/en/authentication/keeping-your-account-and-data-secure/security-log-events'),
)
expect(res.statusCode).toBe(200)
expect(res.headers['content-type']).toContain('text/markdown')
// Check for the main heading
expect(res.body).toContain('# Security log events')
// Check for intro
expect(res.body).toContain(
'Learn about security log events recorded for your personal account.',
)
// Check for manual content section heading
expect(res.body).toContain('## About security log events')
// Check for new main heading
expect(res.body).toContain('## Audit log events')
// Check for category heading
// The template renders "### Category"
expect(res.body).toMatch(/### \w+/)
})
test('Enterprise audit log events page renders with markdown structure', async () => {
const res = await get(
makeURL(
'/en/admin/monitoring-activity-in-your-enterprise/reviewing-audit-logs-for-your-enterprise/audit-log-events-for-your-enterprise',
),
)
expect(res.statusCode).toBe(200)
expect(res.headers['content-type']).toContain('text/markdown')
expect(res.body).toContain('# Audit log events for your enterprise')
})
test('Organization audit log events page renders with markdown structure', async () => {
const res = await get(
makeURL(
'/en/organizations/keeping-your-organization-secure/managing-security-settings-for-your-organization/audit-log-events-for-your-organization',
),
)
expect(res.statusCode).toBe(200)
expect(res.headers['content-type']).toContain('text/markdown')
expect(res.body).toContain('# Audit log events for your organization')
})
test('Events are formatted correctly', async () => {
const res = await get(
makeURL('/en/authentication/keeping-your-account-and-data-secure/security-log-events'),
)
expect(res.statusCode).toBe(200)
// Check for event action header
// #### `action.name`
expect(res.body).toMatch(/#### `[\w.]+`/)
// Check for fields section
expect(res.body).toContain('**Fields:**')
// Check for reference section
expect(res.body).toContain('**Reference:**')
})
test('Manual content is preserved', async () => {
const res = await get(
makeURL('/en/authentication/keeping-your-account-and-data-secure/security-log-events'),
)
expect(res.statusCode).toBe(200)
// The source file has manual content before the marker
expect(res.body).toContain('## About security log events')
})
})

View File

@@ -0,0 +1,139 @@
import type { Context, Page } from '@/types'
import type { PageTransformer } from './types'
import type { CategorizedEvents } from '@/audit-logs/types'
import { renderContent } from '@/content-render/index'
import matter from '@gr2m/gray-matter'
import { readFileSync } from 'fs'
import { join, dirname } from 'path'
import { fileURLToPath } from 'url'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)
/**
* Transformer for Audit Logs pages
* Converts audit log events and their data into markdown format using a Liquid template
*/
export class AuditLogsTransformer implements PageTransformer {
canTransform(page: Page): boolean {
return page.autogenerated === 'audit-logs'
}
async transform(page: Page, pathname: string, context: Context): Promise<string> {
// Import audit log lib dynamically to avoid circular dependencies
const { getCategorizedAuditLogEvents, getCategoryNotes, resolveReferenceLinksToMarkdown } =
await import('@/audit-logs/lib/index')
// Extract version from context
const currentVersion = context.currentVersion!
let pageType = ''
if (pathname.includes('/security-log-events')) {
pageType = 'user'
} else if (pathname.includes('/audit-log-events-for-your-enterprise')) {
pageType = 'enterprise'
} else if (pathname.includes('/audit-log-events-for-your-organization')) {
pageType = 'organization'
} else {
throw new Error(`Unknown audit log page type for path: ${pathname}`)
}
// Get the audit log events data
const categorizedEvents = getCategorizedAuditLogEvents(pageType, currentVersion)
const categoryNotes = getCategoryNotes()
// Prepare manual content
let manualContent = ''
if (page.markdown) {
const markerIndex = page.markdown.indexOf(
'<!-- Content after this section is automatically generated -->',
)
if (markerIndex > 0) {
const { content } = matter(page.markdown)
const manualContentMarkerIndex = content.indexOf(
'<!-- Content after this section is automatically generated -->',
)
if (manualContentMarkerIndex > 0) {
const rawManualContent = content.substring(0, manualContentMarkerIndex).trim()
if (rawManualContent) {
manualContent = await renderContent(rawManualContent, {
...context,
markdownRequested: true,
})
}
}
}
}
// Prepare data for template
const templateData = await this.prepareTemplateData(
page,
categorizedEvents,
categoryNotes,
context,
manualContent,
resolveReferenceLinksToMarkdown,
)
// Load and render template
const templatePath = join(__dirname, '../templates/audit-logs-page.template.md')
const templateContent = readFileSync(templatePath, 'utf8')
// Render the template with Liquid
const rendered = await renderContent(templateContent, {
...context,
...templateData,
markdownRequested: true,
})
return rendered
}
/**
* Prepare data for the Liquid template
*/
private async prepareTemplateData(
page: Page,
categorizedEvents: CategorizedEvents,
categoryNotes: Record<string, string>,
context: Context,
manualContent: string,
resolveReferenceLinksToMarkdown: (docsReferenceLinks: string, context: any) => Promise<string>,
): Promise<Record<string, unknown>> {
// Prepare page intro
const intro = page.intro ? await page.renderProp('intro', context, { textOnly: true }) : ''
// Sort categories and events
const sortedCategorizedEvents: CategorizedEvents = {}
const sortedCategories = Object.keys(categorizedEvents).sort((a, b) => a.localeCompare(b))
for (const category of sortedCategories) {
// Create a copy of the events array to avoid mutating the cache
const events = [...categorizedEvents[category]].sort((a, b) =>
a.action.localeCompare(b.action),
)
sortedCategorizedEvents[category] = await Promise.all(
events.map(async (event) => {
const newEvent = { ...event }
if (newEvent.docs_reference_links && newEvent.docs_reference_links !== 'N/A') {
newEvent.docs_reference_links = await resolveReferenceLinksToMarkdown(
newEvent.docs_reference_links,
context,
)
}
return newEvent
}),
)
}
return {
page: {
title: page.title,
intro,
},
manualContent,
categorizedEvents: sortedCategorizedEvents,
categoryNotes,
}
}
}

View File

@@ -1,5 +1,6 @@
import { TransformerRegistry } from './types'
import { RestTransformer } from './rest-transformer'
import { AuditLogsTransformer } from './audit-logs-transformer'
import { GraphQLTransformer } from './graphql-transformer'
/**
@@ -8,15 +9,9 @@ import { GraphQLTransformer } from './graphql-transformer'
*/
export const transformerRegistry = new TransformerRegistry()
// Register REST transformer
transformerRegistry.register(new RestTransformer())
// Register GraphQL transformer
transformerRegistry.register(new AuditLogsTransformer())
transformerRegistry.register(new GraphQLTransformer())
// Future transformers can be registered here:
// transformerRegistry.register(new WebhooksTransformer())
// transformerRegistry.register(new GitHubAppsTransformer())
export { TransformerRegistry } from './types'
export type { PageTransformer } from './types'

View File

@@ -31,11 +31,61 @@ export function getCategoryNotes(): CategoryNotes {
return auditLogConfig.categoryNotes || {}
}
type TitleResolutionContext = Context & {
export type TitleResolutionContext = Context & {
pages: Record<string, Page>
redirects: Record<string, string>
}
// Resolves docs_reference_links URLs to markdown links
export async function resolveReferenceLinksToMarkdown(
docsReferenceLinks: string,
context: TitleResolutionContext,
): Promise<string> {
if (!docsReferenceLinks || docsReferenceLinks === 'N/A') {
return ''
}
// Handle multiple comma-separated or space-separated links
const links = docsReferenceLinks
.split(/[,\s]+/)
.map((link) => link.trim())
.filter((link) => link && link !== 'N/A')
const markdownLinks = []
for (const link of links) {
try {
const page = findPage(link, context.pages, context.redirects)
if (page) {
// Create a minimal context for rendering the title
const renderContext = {
currentLanguage: 'en',
currentVersion: 'free-pro-team@latest',
pages: context.pages,
redirects: context.redirects,
} as unknown as Context
const title = await page.renderProp('title', renderContext, { textOnly: true })
markdownLinks.push(`[${title}](${link})`)
} else {
// If we can't resolve the link, use the original URL
markdownLinks.push(link)
}
} catch (error) {
// If resolution fails, use the original URL
console.warn(
`Failed to resolve title for link: ${link}`,
error instanceof Error
? error instanceof Error
? error.message
: String(error)
: String(error),
)
markdownLinks.push(link)
}
}
return markdownLinks.join(', ')
}
// Resolves docs_reference_links URLs to page titles
async function resolveReferenceLinksToTitles(
docsReferenceLinks: string,

View File

@@ -0,0 +1,14 @@
---
title: Enterprise administrator documentation
shortTitle: Enterprise administrators
intro: 'Documentation and guides for enterprise administrators.'
changelog:
label: enterprise
layout: product-landing
versions:
ghec: '*'
ghes: '*'
children:
- /monitoring-activity-in-your-enterprise
---

View File

@@ -0,0 +1,14 @@
---
title: Monitoring activity in your enterprise
intro: 'You can view user and system activity by leveraging audit logs.'
redirect_from:
- /enterprise/admin/installation/monitoring-activity-on-your-github-enterprise-server-instance
versions:
ghec: '*'
ghes: '*'
topics:
- Enterprise
children:
- /reviewing-audit-logs-for-your-enterprise
shortTitle: Monitor user activity
---

View File

@@ -0,0 +1,31 @@
---
title: Audit log events for your enterprise
intro: Review the events recorded in an enterprise's audit log.
shortTitle: Audit log events
permissions: 'Enterprise owners {% ifversion ghes %}and site administrators {% endif %}'
redirect_from:
- /enterprise/admin/articles/audited-actions
- /enterprise/admin/installation/audited-actions
- /enterprise/admin/user-management/audited-actions
- /admin/user-management/audited-actions
- /admin/user-management/monitoring-activity-in-your-enterprise/audited-actions
versions:
ghec: '*'
ghes: '*'
type: reference
topics:
- Auditing
- Enterprise
- Logging
- Security
autogenerated: audit-logs
---
> [!NOTE] This article lists events that may appear in the audit log for an **enterprise**. For the events that can appear in a user account's security log or the audit log for an organization, see [AUTOTITLE](/authentication/keeping-your-account-and-data-secure/security-log-events) and [AUTOTITLE](/organizations/keeping-your-organization-secure/managing-security-settings-for-your-organization/audit-log-events-for-your-organization).
## What types of events are included?
* **Without Enterprise Managed Users**, the audit log only includes events related to the enterprise account and the organizations within it.
* **With Enterprise Managed Users**, the audit log also includes user events, which are not listed here. For that list, see [AUTOTITLE](/authentication/keeping-your-account-and-data-secure/security-log-events).
<!-- Content after this section is automatically generated -->

View File

@@ -0,0 +1,13 @@
---
title: Reviewing audit logs for your enterprise
intro: You can view user and system activity in the audit logs for your enterprise.
shortTitle: Review audit logs
versions:
ghec: '*'
ghes: '*'
topics:
- Enterprise
children:
- /audit-log-events-for-your-enterprise
---

View File

@@ -0,0 +1,33 @@
---
title: Authentication documentation
shortTitle: Authentication
intro: 'Keep your account and data secure with features like two-factor authentication, SSH, and commit signature verification.'
redirect_from:
- /categories/56/articles
- /categories/ssh
- /mac-verify-ssh
- /ssh-issues
- /verify-ssh-redirect
- /win-verify-ssh
- /categories/92/articles
- /categories/gpg
- /categories/security
- /categories/authenticating-to-github
- /github/authenticating-to-github
versions:
fpt: '*'
ghes: '*'
ghec: '*'
changelog:
label: '2FA,authentication,security keys,SSH,token authentication'
layout: product-landing
topics:
- 2FA
- Identity
- Access management
- Usernames
- Device verification
children:
- /keeping-your-account-and-data-secure
---

View File

@@ -0,0 +1,17 @@
---
title: Keeping your account and data secure
intro: 'To protect your personal information, you should keep both your account on GitHub and any associated data secure.'
redirect_from:
- /articles/keeping-your-account-and-data-secure
- /github/authenticating-to-github/keeping-your-account-and-data-secure
versions:
fpt: '*'
ghes: '*'
ghec: '*'
topics:
- Identity
- Access management
children:
- /security-log-events
shortTitle: Account security
---

View File

@@ -0,0 +1,21 @@
---
title: Security log events
intro: Learn about security log events recorded for your personal account.
versions:
fpt: '*'
ghes: '*'
ghec: '*'
topics:
- Identity
- Access management
autogenerated: audit-logs
---
> [!NOTE]
> This article contains the events that may appear in your user account's security log.
## About security log events
Reference grouped by category.
<!-- Content after this section is automatically generated -->

View File

@@ -35,12 +35,12 @@ children:
- graphql
- video-transcripts
# - account-and-profile
# - authentication
- authentication
# - repositories
# - admin
- admin
# - billing
# - site-policy
# - organizations
- organizations
# - pull-requests
# - issues
# - copilot

View File

@@ -0,0 +1,21 @@
---
title: Organizations and teams documentation
shortTitle: Organizations
intro: 'You can use organizations to collaborate with a large number of people across many projects at once, while managing access to your data and customizing settings.'
redirect_from:
- /articles/about-improved-organization-permissions
- /categories/setting-up-and-managing-organizations-and-teams
- /github/setting-up-and-managing-organizations-and-teams
- /organizations/organizing-members-into-teams/disabling-team-discussions-for-your-organization
layout: product-landing
versions:
fpt: '*'
ghes: '*'
ghec: '*'
topics:
- Organizations
- Teams
children:
- /keeping-your-organization-secure
---

View File

@@ -0,0 +1,18 @@
---
title: Keeping your organization secure
intro: 'You can harden security for your organization by managing security settings, requiring two-factor authentication (2FA), and reviewing the activity and integrations within your organization.'
redirect_from:
- /articles/preventing-unauthorized-access-to-organization-information
- /articles/keeping-your-organization-secure
- /github/setting-up-and-managing-organizations-and-teams/keeping-your-organization-secure
versions:
fpt: '*'
ghes: '*'
ghec: '*'
topics:
- Organizations
- Teams
children:
- /managing-security-settings-for-your-organization
shortTitle: Organization security
---

View File

@@ -0,0 +1,22 @@
---
title: Audit log events for your organization
intro: Learn about audit log events recorded for your organization.
versions:
fpt: '*'
ghes: '*'
ghec: '*'
topics:
- Organizations
- Teams
shortTitle: Audit log events
autogenerated: audit-logs
---
> [!NOTE]
> This article contains the events that may appear in your organization's audit log.
## About audit log events for your organization
Reference grouped by category.
<!-- Content after this section is automatically generated -->

View File

@@ -0,0 +1,14 @@
---
title: Managing security settings for your organization
shortTitle: Manage security settings
intro: 'You can manage security settings and review the audit log and integrations for your organization.'
versions:
fpt: '*'
ghes: '*'
ghec: '*'
topics:
- Organizations
- Teams
children:
- /audit-log-events-for-your-organization
---