fix(security): resolve code scanning alerts

Add least-privilege 'permissions: contents: read' to quality, security
and docker-ci workflows (actions/missing-workflow-permissions).

Harden markdown table cell escaping in exportMarkdown: escape backslash
before pipe and collapse newlines so untrusted values can't break the
table (js/incomplete-sanitization). Add regression tests.

ha-relevant: maybe
This commit is contained in:
Pouzor
2026-06-09 16:51:32 +02:00
parent ea66e6c9c7
commit 7c2417f5a9
5 changed files with 42 additions and 3 deletions
+3
View File
@@ -6,6 +6,9 @@ on:
pull_request: pull_request:
branches: [main] branches: [main]
permissions:
contents: read
jobs: jobs:
smoke-and-integration: smoke-and-integration:
runs-on: ubuntu-latest runs-on: ubuntu-latest
+3
View File
@@ -6,6 +6,9 @@ on:
pull_request: pull_request:
branches: [main] branches: [main]
permissions:
contents: read
jobs: jobs:
lint-scripts: lint-scripts:
runs-on: ubuntu-latest runs-on: ubuntu-latest
+3
View File
@@ -8,6 +8,9 @@ on:
schedule: schedule:
- cron: '0 9 * * 1' # Weekly on Monday - cron: '0 9 * * 1' # Weekly on Monday
permissions:
contents: read
jobs: jobs:
secrets-scan: secrets-scan:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -53,6 +53,33 @@ describe('generateMarkdownTable', () => {
expect(md).toContain('A\\|B') expect(md).toContain('A\\|B')
}) })
it('escapes backslashes before pipes so the escape char is not ambiguous', () => {
const nodes = [makeNode({ label: 'A\\|B' })]
const md = generateMarkdownTable(nodes)
// backslash doubled, then the literal pipe escaped
expect(md).toContain('A\\\\\\|B')
})
it('collapses newlines in cell values so they do not break the table', () => {
const nodes = [makeNode({ label: 'line1\nline2', hostname: 'a\r\nb' })]
const lines = generateMarkdownTable(nodes).split('\n')
// header + separator + exactly one data row (no extra line from the value)
expect(lines).toHaveLength(3)
expect(lines[2]).toContain('line1 line2')
expect(lines[2]).toContain('a b')
})
it('escapes pipe characters inside service names', () => {
const nodes = [makeNode({
label: 'Server',
services: [{ port: 80, protocol: 'tcp', service_name: 'web|proxy' }],
})]
const lines = generateMarkdownTable(nodes).split('\n')
// header + separator + exactly one data row — the pipe must not add a column
expect(lines).toHaveLength(3)
expect(lines[2]).toContain('web\\|proxy')
})
it('generates one row per non-groupRect node', () => { it('generates one row per non-groupRect node', () => {
const nodes = [ const nodes = [
makeNode({ type: 'server', label: 'A' }, '1'), makeNode({ type: 'server', label: 'A' }, '1'),
+6 -3
View File
@@ -5,8 +5,11 @@ const EMPTY = '—'
function cell(v: string | null | undefined): string { function cell(v: string | null | undefined): string {
if (!v) return EMPTY if (!v) return EMPTY
// Escape pipe chars so they don't break the table // Escape backslashes first, then pipes, and collapse newlines so they don't break the table
return v.replace(/\|/g, '\\|') return v
.replace(/\\/g, '\\\\')
.replace(/\|/g, '\\|')
.replace(/\r?\n/g, ' ')
} }
export function generateMarkdownTable(nodes: Node<NodeData>[]): string { export function generateMarkdownTable(nodes: Node<NodeData>[]): string {
@@ -27,7 +30,7 @@ export function generateMarkdownTable(nodes: Node<NodeData>[]): string {
cell(d.ip), cell(d.ip),
cell(d.hostname), cell(d.hostname),
cell(d.status), cell(d.status),
services, cell(services),
] ]
}) })