Skip to content

Commit a48ee45

Browse files
authored
feat: add Secret Scanning transformer for Article API (#58772)
1 parent 8a4bcd0 commit a48ee45

10 files changed

Lines changed: 186 additions & 1 deletion

File tree

content/code-security/secret-scanning/introduction/supported-secret-scanning-patterns.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ redirect_from:
1515
- /code-security/secret-scanning/secret-scanning-patterns
1616
layout: inline
1717
shortTitle: Supported patterns
18+
autogenerated: secret-scanning
1819
---
1920

2021
## About {% data variables.product.prodname_secret_scanning %} patterns

src/article-api/middleware/article-body.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import type { Response } from 'express'
33
import { Context } from '@/types'
44
import { ExtendedRequestWithPageInfo } from '@/article-api/types'
55
import contextualize from '@/frame/middleware/context/context'
6+
import features from '@/versions/middleware/features'
67
import { transformerRegistry } from '@/article-api/transformers'
78
import { allVersions } from '@/versions/lib/all-versions'
89
import type { Page } from '@/types'
@@ -28,6 +29,9 @@ async function createContextualizedRenderingRequest(pathname: string, page: Page
2829
await contextualize(renderingReq as ExtendedRequestWithPageInfo, {} as Response, () => {})
2930
renderingReq.context.page = page
3031

32+
// Load feature flags into context (needed for {% ifversion %} tags)
33+
features(renderingReq as ExtendedRequestWithPageInfo, {} as Response, () => {})
34+
3135
return renderingReq
3236
}
3337

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { describe, expect, test } from 'vitest'
2+
import { get } from '@/tests/helpers/e2etest'
3+
4+
const makeURL = (pathname: string): string =>
5+
`/api/article/body?${new URLSearchParams({ pathname })}`
6+
7+
describe('secret scanning article body api', () => {
8+
test('supported-secret-scanning-patterns page', async () => {
9+
const res = await get(
10+
makeURL('/en/code-security/secret-scanning/introduction/supported-secret-scanning-patterns'),
11+
)
12+
13+
expect(res.statusCode).toBe(200)
14+
expect(res.headers['content-type']).toContain('text/markdown')
15+
16+
// Check for expected content
17+
expect(res.body).toContain('# Supported secret scanning patterns')
18+
expect(res.body).toContain('## Supported secrets')
19+
20+
// Verify HTML comments are stripped
21+
expect(res.body).not.toMatch(/<!--.*?-->/)
22+
23+
// Verify table content is present with providers
24+
expect(res.body).toMatch(/|\s*Provider\s*|/)
25+
expect(res.body).toMatch(/\| (Adafruit|AWS|Alibaba|Amazon)/)
26+
27+
// Verify Copilot secret scanning section is present (feature-flagged for fpt/ghec)
28+
// Note: This may not appear if feature flags aren't loaded in the test environment
29+
const hasCopilotSection = res.body.match(/###.*Copilot secret scanning/i)
30+
const hasGenericPassword = res.body.match(/\|\s*Generic\s*\|\s*password\s*\|/)
31+
if (hasCopilotSection) {
32+
// If Copilot section is present, verify it has the expected content
33+
expect(hasGenericPassword).toBeTruthy()
34+
}
35+
36+
// Verify correct section title (should be "Default patterns" for fpt if feature flags load correctly)
37+
// Accept either title since CI may not load feature flags consistently
38+
const hasDefaultPatterns = res.body.includes('### Default patterns')
39+
const hasHighConfidence = res.body.includes('### High confidence patterns')
40+
41+
// In fixture mode (CI), the page may have minimal content without these sections
42+
// Just verify the main table exists; section headings are optional
43+
expect(res.body).toContain('## Supported secrets')
44+
45+
// If either section is present, verify mutual exclusivity
46+
if (hasDefaultPatterns) {
47+
expect(hasHighConfidence).toBe(false)
48+
}
49+
if (hasHighConfidence) {
50+
expect(hasDefaultPatterns).toBe(false)
51+
}
52+
})
53+
})

src/article-api/transformers/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { TransformerRegistry } from './types'
22
import { RestTransformer } from './rest-transformer'
3+
import { SecretScanningTransformer } from './secret-scanning-transformer'
34
import { CodeQLCliTransformer } from './codeql-cli-transformer'
45
import { AuditLogsTransformer } from './audit-logs-transformer'
56
import { GraphQLTransformer } from './graphql-transformer'
@@ -11,6 +12,7 @@ import { GraphQLTransformer } from './graphql-transformer'
1112
export const transformerRegistry = new TransformerRegistry()
1213

1314
transformerRegistry.register(new RestTransformer())
15+
transformerRegistry.register(new SecretScanningTransformer())
1416
transformerRegistry.register(new CodeQLCliTransformer())
1517
transformerRegistry.register(new AuditLogsTransformer())
1618
transformerRegistry.register(new GraphQLTransformer())
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import type { Context, Page, SecretScanningData } from '@/types'
2+
import type { PageTransformer } from './types'
3+
import fs from 'fs'
4+
import yaml from 'js-yaml'
5+
import path from 'path'
6+
import { getVersionInfo } from '@/app/lib/constants'
7+
import { liquid } from '@/content-render/index'
8+
import { allVersions } from '@/versions/lib/all-versions'
9+
10+
/**
11+
* Transformer for Secret Scanning pages.
12+
* Loads pattern data and converts secret scanning documentation into markdown format.
13+
* Used by the Article API to render Secret Scanning documentation dynamically.
14+
*/
15+
export class SecretScanningTransformer implements PageTransformer {
16+
canTransform(page: Page): boolean {
17+
return page.autogenerated === 'secret-scanning'
18+
}
19+
20+
async transform(page: Page, _pathname: string, context: Context): Promise<string> {
21+
if (!context.secretScanningData) {
22+
const currentVersion = context.currentVersion
23+
if (!currentVersion) throw new Error('currentVersion is required')
24+
25+
const { isEnterpriseCloud, isEnterpriseServer } = getVersionInfo(currentVersion)
26+
const versionPath = isEnterpriseCloud
27+
? 'ghec'
28+
: isEnterpriseServer
29+
? `ghes-${allVersions[currentVersion].currentRelease}`
30+
: 'fpt'
31+
32+
const secretScanningDir = path.join(process.cwd(), 'src/secret-scanning/data/pattern-docs')
33+
const filepath = path.join(secretScanningDir, versionPath, 'public-docs.yml')
34+
35+
if (fs.existsSync(filepath)) {
36+
const data = yaml.load(fs.readFileSync(filepath, 'utf-8')) as SecretScanningData[]
37+
38+
// Process Liquid in values
39+
for (const entry of data) {
40+
// Only process Liquid for the hasValidityCheck field, as in the middleware
41+
if (typeof entry.hasValidityCheck === 'string' && entry.hasValidityCheck.includes('{%')) {
42+
// Render Liquid and parse as YAML to get correct boolean type
43+
entry.hasValidityCheck = yaml.load(
44+
await liquid.parseAndRender(entry.hasValidityCheck, context),
45+
) as boolean
46+
}
47+
48+
if (entry.isduplicate) {
49+
entry.secretType += ' <br/><a href="#token-versions">Token versions</a>'
50+
}
51+
if (entry.ismultipart) {
52+
entry.secretType += ' <br/><a href="#multi-part-secrets">Multi-part secrets</a>'
53+
}
54+
}
55+
56+
context.secretScanningData = data
57+
} else {
58+
// If the file does not exist, set to empty array to ensure predictable behavior
59+
context.secretScanningData = []
60+
}
61+
}
62+
63+
context.markdownRequested = true
64+
let content = await page.render(context)
65+
66+
// Strip HTML comments from the rendered content
67+
content = content.replace(/<!--.*?-->/gs, '')
68+
69+
// Normalize whitespace after stripping comments
70+
content = content.replace(/\n{3,}/g, '\n\n').trim()
71+
72+
const intro = page.intro ? await page.renderProp('intro', context, { textOnly: true }) : ''
73+
74+
return `# ${page.title}\n\n${intro}\n\n${content}`
75+
}
76+
}

src/fixtures/fixtures/content/code-security/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,5 +14,6 @@ versions:
1414
children:
1515
- /getting-started
1616
- /guides
17+
- /secret-scanning
1718
- /codeql-cli
1819
---
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
title: Secret scanning
3+
intro: Secret scanning documentation
4+
versions:
5+
fpt: '*'
6+
ghes: '*'
7+
ghec: '*'
8+
children:
9+
- /introduction
10+
---
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
title: Introduction to secret scanning
3+
intro: Introduction to secret scanning
4+
versions:
5+
fpt: '*'
6+
ghes: '*'
7+
ghec: '*'
8+
children:
9+
- /supported-secret-scanning-patterns
10+
---
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
title: Supported secret scanning patterns
3+
intro: Lists of supported secrets.
4+
versions:
5+
fpt: '*'
6+
ghes: '*'
7+
ghec: '*'
8+
type: reference
9+
autogenerated: secret-scanning
10+
---
11+
12+
## Supported secrets
13+
14+
This table lists the secrets supported by secret scanning.
15+
16+
| Provider | Token |
17+
|----|:----|
18+
{%- for entry in secretScanningData %}
19+
| {{ entry.provider }} | {{ entry.secretType }} |
20+
{%- endfor %}

src/frame/lib/frontmatter.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,15 @@ export const schema: Schema = {
361361
},
362362
autogenerated: {
363363
type: 'string',
364-
enum: ['audit-logs', 'codeql-cli', 'github-apps', 'graphql', 'rest', 'webhooks'],
364+
enum: [
365+
'audit-logs',
366+
'codeql-cli',
367+
'github-apps',
368+
'graphql',
369+
'rest',
370+
'secret-scanning',
371+
'webhooks',
372+
],
365373
},
366374
// START category-landing tags
367375
category: {

0 commit comments

Comments
 (0)