feat: add AuditLogsTransformer for Article API (#58754)
This commit is contained in:
30
src/article-api/templates/audit-logs-page.template.md
Normal file
30
src/article-api/templates/audit-logs-page.template.md
Normal 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 %}
|
||||
95
src/article-api/tests/audit-logs-transformer.ts
Normal file
95
src/article-api/tests/audit-logs-transformer.ts
Normal 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')
|
||||
})
|
||||
})
|
||||
139
src/article-api/transformers/audit-logs-transformer.ts
Normal file
139
src/article-api/transformers/audit-logs-transformer.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
|
||||
14
src/fixtures/fixtures/content/admin/index.md
Normal file
14
src/fixtures/fixtures/content/admin/index.md
Normal 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
|
||||
---
|
||||
@@ -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
|
||||
---
|
||||
@@ -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 -->
|
||||
@@ -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
|
||||
---
|
||||
|
||||
33
src/fixtures/fixtures/content/authentication/index.md
Normal file
33
src/fixtures/fixtures/content/authentication/index.md
Normal 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
|
||||
---
|
||||
@@ -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
|
||||
---
|
||||
@@ -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 -->
|
||||
@@ -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
|
||||
|
||||
21
src/fixtures/fixtures/content/organizations/index.md
Normal file
21
src/fixtures/fixtures/content/organizations/index.md
Normal 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
|
||||
---
|
||||
@@ -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
|
||||
---
|
||||
@@ -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 -->
|
||||
@@ -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
|
||||
---
|
||||
Reference in New Issue
Block a user