Skip to content

Commit 438d72b

Browse files
committed
fix: corrected security advisory parser
1 parent afb9571 commit 438d72b

File tree

3 files changed

+106
-84
lines changed

3 files changed

+106
-84
lines changed

src/app/security/[advisory]/page.tsx

Lines changed: 4 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { join } from 'path'
44
import PageLayout from '@/components/PageLayout'
55
import { generatePageMetadata } from '@/components/PageLayout'
66
import { Metadata } from 'next'
7+
import { parseAdvisoryToHTML } from '@/lib/securityAdvisory'
78

89
interface SecurityAdvisoryPageProps {
910
params: Promise<{
@@ -38,87 +39,6 @@ function getAdvisoryContent(advisory: string): { title: string; description: str
3839
}
3940
}
4041

41-
// Function to convert Markdown content to JSX
42-
function parseAdvisoryContent(content: string): React.JSX.Element {
43-
// Robust parser: group consecutive metadata lines into a single <dl class="dl-horizontal">
44-
const lines = content.split('\n')
45-
46-
const htmlParts: string[] = []
47-
let inDl = false
48-
49-
const closeDlIfOpen = () => {
50-
if (inDl) {
51-
htmlParts.push('</dl>')
52-
inDl = false
53-
}
54-
}
55-
56-
for (let line of lines) {
57-
let l = line.trim()
58-
59-
// Skip empty lines, but ensure we close an open DL
60-
if (l.length === 0) {
61-
closeDlIfOpen()
62-
continue
63-
}
64-
65-
// Keep raw HTML intact
66-
if (l.startsWith('<')) {
67-
closeDlIfOpen()
68-
htmlParts.push(l)
69-
continue
70-
}
71-
72-
// Headings
73-
if (l.startsWith('#### ')) {
74-
closeDlIfOpen()
75-
htmlParts.push(`<h4>${l.substring(5)}</h4>`)
76-
continue
77-
}
78-
if (l.startsWith('### ')) {
79-
closeDlIfOpen()
80-
htmlParts.push(`<h3>${l.substring(4)}</h3>`)
81-
continue
82-
}
83-
if (l.startsWith('## ')) {
84-
closeDlIfOpen()
85-
htmlParts.push(`<h2>${l.substring(3)}</h2>`)
86-
continue
87-
}
88-
89-
// Metadata lines like **Issued on::** 2004-07-27 or **Risk:** medium
90-
// Strategy: capture bold label and the value; strip trailing colons from label
91-
const metaMatch = l.match(/^\*\*(.+?)\*\*\s*(.+)$/)
92-
if (metaMatch) {
93-
let labelRaw = metaMatch[1].trim()
94-
const value = metaMatch[2].trim()
95-
96-
// Accept labels with one or more trailing colons inside the bold
97-
labelRaw = labelRaw.replace(/:+\s*$/, '').trim()
98-
99-
if (!inDl) {
100-
htmlParts.push('<dl class="dl-horizontal">')
101-
inDl = true
102-
}
103-
104-
htmlParts.push(`<dt>${labelRaw}:</dt><dd>${value}</dd>`)
105-
continue
106-
}
107-
108-
// Default: wrap as paragraph
109-
closeDlIfOpen()
110-
htmlParts.push(`<p>${l}</p>`)
111-
}
112-
113-
// Close any open DL at the end
114-
if (inDl) {
115-
htmlParts.push('</dl>')
116-
}
117-
118-
const processedContent = htmlParts.join('\n').replace(/\n+/g, '\n').trim()
119-
return <div dangerouslySetInnerHTML={{ __html: processedContent }} />
120-
}
121-
12242
export async function generateStaticParams() {
12343
// Read all security advisory files from the content/security directory
12444
const { readdirSync } = await import('fs')
@@ -166,8 +86,8 @@ export default async function SecurityAdvisoryPage({ params }: SecurityAdvisoryP
16686
notFound()
16787
}
16888

169-
const contentElement = parseAdvisoryContent(advisoryData.content)
170-
89+
const html = parseAdvisoryToHTML(advisoryData.content)
90+
17191
return (
17292
<PageLayout title={advisoryData.title}>
17393
<div className="row">
@@ -179,7 +99,7 @@ export default async function SecurityAdvisoryPage({ params }: SecurityAdvisoryP
17999
</ol>
180100
</nav>
181101

182-
{contentElement}
102+
<div dangerouslySetInnerHTML={{ __html: html }} />
183103
</div>
184104
</div>
185105
</PageLayout>

src/lib/securityAdvisory.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// Parser für Security-Advisory-Markdown in einen HTML-String
2+
// - Gruppiert aufeinanderfolgende Metadaten-Zeilen (**Label:** Wert) in ein gemeinsames <dl class="dl-horizontal">
3+
// - Belässt vorhandenes HTML unverändert
4+
// - Unterstützt Überschriften (##/###/####) und einfache Absätze pro Zeile
5+
6+
export function parseAdvisoryToHTML(content: string): string {
7+
const lines = content.split('\n')
8+
const htmlParts: string[] = []
9+
let inDl = false
10+
11+
const closeDlIfOpen = () => {
12+
if (inDl) {
13+
htmlParts.push('</dl>')
14+
inDl = false
15+
}
16+
}
17+
18+
for (let rawLine of lines) {
19+
const l = rawLine.trim()
20+
21+
if (l.length === 0) {
22+
closeDlIfOpen()
23+
continue
24+
}
25+
26+
// Bewahre vorhandenes HTML
27+
if (l.startsWith('<')) {
28+
closeDlIfOpen()
29+
htmlParts.push(l)
30+
continue
31+
}
32+
33+
// Überschriften
34+
if (l.startsWith('#### ')) {
35+
closeDlIfOpen()
36+
htmlParts.push(`<h4>${l.substring(5)}</h4>`)
37+
continue
38+
}
39+
if (l.startsWith('### ')) {
40+
closeDlIfOpen()
41+
htmlParts.push(`<h3>${l.substring(4)}</h3>`)
42+
continue
43+
}
44+
if (l.startsWith('## ')) {
45+
closeDlIfOpen()
46+
htmlParts.push(`<h2>${l.substring(3)}</h2>`)
47+
continue
48+
}
49+
50+
// Metadaten-Zeilen wie **Issued on::** 2004-07-27
51+
const metaMatch = l.match(/^\*\*(.+?)\*\*\s*(.+)$/)
52+
if (metaMatch) {
53+
let labelRaw = metaMatch[1].trim()
54+
const value = metaMatch[2].trim()
55+
56+
// Entferne einen oder mehrere abschließende Doppelpunkte innerhalb des Labels
57+
labelRaw = labelRaw.replace(/:+\s*$/, '').trim()
58+
59+
if (!inDl) {
60+
htmlParts.push('<dl class="dl-horizontal">')
61+
inDl = true
62+
}
63+
64+
htmlParts.push(`<dt>${labelRaw}:</dt><dd>${value}</dd>`)
65+
continue
66+
}
67+
68+
// Standard: Absatz
69+
closeDlIfOpen()
70+
htmlParts.push(`<p>${l}</p>`)
71+
}
72+
73+
// Offenes DL schließen
74+
closeDlIfOpen()
75+
76+
return htmlParts.join('\n').replace(/\n+/g, '\n').trim()
77+
}
78+
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { describe, it, expect } from 'vitest'
2+
import { parseAdvisoryToHTML } from '@/lib/securityAdvisory'
3+
4+
describe('parseAdvisoryContent', () => {
5+
it('groups metadata lines into a single dl with correct dt/dd pairs', () => {
6+
const md = `## Vulnerability in phpMyFAQ version 1.4.0
7+
**Issued on::** 2004-07-27
8+
**Software::** phpMyFAQ version 1.4.0
9+
**Risk::** medium
10+
**Platforms::** all
11+
12+
The phpMyFAQ Team has learned of a security vulnerability in phpMyFAQ version 1.4.0.
13+
`
14+
15+
const html = parseAdvisoryToHTML(md)
16+
17+
expect(html).toContain('<h2>Vulnerability in phpMyFAQ version 1.4.0</h2>')
18+
expect(html).toContain('<dl class="dl-horizontal">')
19+
expect(html).toContain('<dt>Issued on:</dt><dd>2004-07-27</dd>')
20+
expect(html).toContain('<dt>Software:</dt><dd>phpMyFAQ version 1.4.0</dd>')
21+
expect(html).toContain('<dt>Risk:</dt><dd>medium</dd>')
22+
expect(html).toContain('<dt>Platforms:</dt><dd>all</dd>')
23+
})
24+
})

0 commit comments

Comments
 (0)