diff --git a/.github/workflows/docker-ci.yml b/.github/workflows/docker-ci.yml index 227be16..dde50c0 100644 --- a/.github/workflows/docker-ci.yml +++ b/.github/workflows/docker-ci.yml @@ -6,6 +6,9 @@ on: pull_request: branches: [main] +permissions: + contents: read + jobs: smoke-and-integration: runs-on: ubuntu-latest diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml index b6a2bd4..6c8ded2 100644 --- a/.github/workflows/quality.yml +++ b/.github/workflows/quality.yml @@ -6,6 +6,9 @@ on: pull_request: branches: [main] +permissions: + contents: read + jobs: lint-scripts: runs-on: ubuntu-latest diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 043efac..3118aec 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -8,6 +8,9 @@ on: schedule: - cron: '0 9 * * 1' # Weekly on Monday +permissions: + contents: read + jobs: secrets-scan: runs-on: ubuntu-latest diff --git a/frontend/src/utils/__tests__/exportMarkdown.test.ts b/frontend/src/utils/__tests__/exportMarkdown.test.ts index 6da24f4..edd52a1 100644 --- a/frontend/src/utils/__tests__/exportMarkdown.test.ts +++ b/frontend/src/utils/__tests__/exportMarkdown.test.ts @@ -53,6 +53,33 @@ describe('generateMarkdownTable', () => { 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', () => { const nodes = [ makeNode({ type: 'server', label: 'A' }, '1'), diff --git a/frontend/src/utils/exportMarkdown.ts b/frontend/src/utils/exportMarkdown.ts index 4dfef69..7205b9f 100644 --- a/frontend/src/utils/exportMarkdown.ts +++ b/frontend/src/utils/exportMarkdown.ts @@ -5,8 +5,11 @@ const EMPTY = '—' function cell(v: string | null | undefined): string { if (!v) return EMPTY - // Escape pipe chars so they don't break the table - return v.replace(/\|/g, '\\|') + // Escape backslashes first, then pipes, and collapse newlines so they don't break the table + return v + .replace(/\\/g, '\\\\') + .replace(/\|/g, '\\|') + .replace(/\r?\n/g, ' ') } export function generateMarkdownTable(nodes: Node[]): string { @@ -27,7 +30,7 @@ export function generateMarkdownTable(nodes: Node[]): string { cell(d.ip), cell(d.hostname), cell(d.status), - services, + cell(services), ] })