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/.gitignore b/.gitignore index 252f8e0..70c818c 100644 --- a/.gitignore +++ b/.gitignore @@ -46,6 +46,7 @@ htmlcov/ *.db-shm *.db-wal *.db.back +*.db.back-* # Docker .docker/ diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 787af1c..1ce9089 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -6196,9 +6196,9 @@ } }, "node_modules/hono": { - "version": "4.12.18", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.18.tgz", - "integrity": "sha512-RWzP96k/yv0PQfyXnWjs6zot20TqfpfsNXhOnev8d1InAxubW93L11/oNUc3tQqn2G0bSdAOBpX+2uDFHV7kdQ==", + "version": "4.12.25", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.25.tgz", + "integrity": "sha512-2NFaIyNVgJmBs/ecmtGzlmluTFs5cHEWGTdu0t1HBwYzoGXOL5nUQBRMXsXWla5i4KkG//QMzVP88m1+I3fdAQ==", "license": "MIT", "engines": { "node": ">=16.9.0" diff --git a/frontend/package.json b/frontend/package.json index 4661af3..80ac42d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -37,6 +37,9 @@ "tw-animate-css": "^1.4.0", "zustand": "^5.0.11" }, + "overrides": { + "hono": "^4.12.21" + }, "devDependencies": { "@eslint/js": "^9.39.1", "@tailwindcss/vite": "^4.2.1", 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), ] })