From 078966b7c38bbbd1ada219b50399ab6546ac2b35 Mon Sep 17 00:00:00 2001 From: Kyle Brown Date: Sat, 15 Aug 2026 05:46:08 +0000 Subject: [PATCH 1/4] feat(sdk): prepare security findings for publication --- sdk/typescript/scripts/check-package.mjs | 1 + sdk/typescript/src/publication.ts | 288 +++++++++++++++++ sdk/typescript/tests-ts/publication.test.ts | 330 ++++++++++++++++++++ 3 files changed, 619 insertions(+) create mode 100644 sdk/typescript/src/publication.ts create mode 100644 sdk/typescript/tests-ts/publication.test.ts diff --git a/sdk/typescript/scripts/check-package.mjs b/sdk/typescript/scripts/check-package.mjs index a98cc517..d13ac5f2 100644 --- a/sdk/typescript/scripts/check-package.mjs +++ b/sdk/typescript/scripts/check-package.mjs @@ -172,6 +172,7 @@ const distFiles = new Set( "knowledge-base", "models", "multiscan", + "publication", "result", "runtime", "scan-activity", diff --git a/sdk/typescript/src/publication.ts b/sdk/typescript/src/publication.ts new file mode 100644 index 00000000..d6bc8334 --- /dev/null +++ b/sdk/typescript/src/publication.ts @@ -0,0 +1,288 @@ +import { resolve } from "node:path"; +import { loadContract, type LoadedContract } from "./contract.js"; +import type { + Finding, + FindingCodeEvidence, + FindingLocation, + ScanTargetRecord, + SeverityLevel, +} from "./models.js"; +import { bundledPluginRoot } from "./runtime.js"; + +export interface LinearPublicationDestination { + type: "linear"; + teamId: string; + projectId: string; +} + +export interface PrepareScanPublicationOptions { + destination: "linear"; + teamId: string; + projectId: string; + uploadedAt?: string; +} + +export interface PreparedPublicationIssue { + findingId: string; + occurrenceId: string; + title: string; + description: string; + priority?: 1 | 2 | 3 | 4; +} + +export interface PreparedScanPublication { + scanId: string; + uploadId: string; + scanDirectory: string; + destination: LinearPublicationDestination; + issues: PreparedPublicationIssue[]; +} + +const LINEAR_PRIORITIES = { + critical: 1, + high: 2, + medium: 3, + low: 4, + informational: undefined, +} as const satisfies Record; + +export async function prepareScanPublication( + scanDirectory: string, + options: PrepareScanPublicationOptions, +): Promise { + const contract = await loadContract(scanDirectory, { + pluginRoot: await bundledPluginRoot(), + }); + const uploadedAt = options.uploadedAt ?? new Date().toISOString(); + const scanId = contract.manifest.scan.id; + + return { + scanId, + uploadId: scanId, + scanDirectory: resolve(scanDirectory), + destination: { + type: options.destination, + teamId: options.teamId, + projectId: options.projectId, + }, + issues: contract.findings.findings.map((finding) => { + const priority = LINEAR_PRIORITIES[finding.severity.level]; + return { + findingId: finding.findingId, + occurrenceId: finding.occurrenceId, + title: `[Codex Security][${finding.severity.level.toUpperCase()}] ${finding.title}`, + description: renderFindingDescription(contract, finding, uploadedAt), + ...(priority === undefined ? {} : { priority }), + }; + }), + }; +} + +function renderFindingDescription( + contract: LoadedContract, + finding: Finding, + uploadedAt: string, +): string { + const { coverage } = contract; + const { scan } = contract.manifest; + const lines = [ + "## Codex Security finding", + "", + `**Scan ID:** ${scan.id}`, + `**Upload ID:** ${scan.id}`, + `**Finding ID:** ${finding.findingId}`, + `**Occurrence ID:** ${finding.occurrenceId}`, + `**Fingerprint:** ${finding.fingerprints.primary}`, + `**Severity:** ${finding.severity.level.toUpperCase()}`, + `**Confidence:** ${finding.confidence.level.toUpperCase()}`, + ...(finding.taxonomy.cwe.length === 0 + ? [] + : [`**CWE:** ${finding.taxonomy.cwe.join(", ")}`]), + "", + "## Scanned code", + "", + `**Repository:** ${scan.target.displayName}`, + ...(scan.target.remote === undefined + ? [] + : [`**Remote:** ${scan.target.remote}`]), + ...renderTargetIdentity(scan.target), + `**Scanned scope:** ${scan.scope.includePaths.join(", ") || "entire repository"}`, + ...(scan.scope.excludePaths.length === 0 + ? [] + : [`**Excluded scope:** ${scan.scope.excludePaths.join(", ")}`]), + `**Coverage:** ${coverage.completeness}`, + `**Coverage mode:** ${coverage.mode}`, + `**Scan mode:** ${scanMode(coverage.mode)}`, + `**Started:** ${scan.startedAt}`, + `**Completed:** ${scan.completedAt}`, + `**Uploaded:** ${uploadedAt}`, + "", + "### Affected locations", + "", + ...finding.locations.map((location) => + renderLocation(scan.target, location), + ), + "", + "## Summary", + "", + finding.summary, + ]; + + const rootCause = finding.rootCause; + if (typeof rootCause === "string") { + lines.push("", "## Root cause", "", rootCause); + } else if (rootCause !== undefined) { + lines.push("", "## Root cause", "", rootCause.summary); + if (rootCause.code !== undefined) { + lines.push("", fencedCode(rootCause.code, rootCause.language)); + } + } + + if (finding.codeEvidence !== undefined && finding.codeEvidence.length > 0) { + lines.push("", "## Source-code evidence"); + for (const evidence of finding.codeEvidence) { + lines.push("", ...renderCodeEvidence(scan.target, evidence)); + } + } + + lines.push("", "## Remediation", "", finding.remediation); + return `${lines.join("\n")}\n`; +} + +function renderTargetIdentity(target: ScanTargetRecord): string[] { + const lines: string[] = []; + if (target.revision !== undefined) { + const label = target.kind === "git_revision" ? "Revision" : "Base revision"; + lines.push(`**${label}:** ${target.revision}`); + } + if (target.baseRevision !== undefined) { + lines.push(`**Diff base revision:** ${target.baseRevision}`); + } + if (target.headRevision !== undefined) { + lines.push(`**Diff head revision:** ${target.headRevision}`); + } + if (target.snapshotDigest !== undefined) { + lines.push(`**Snapshot digest:** ${target.snapshotDigest}`); + } + return lines; +} + +function scanMode(mode: LoadedContract["coverage"]["mode"]): string { + if (mode === "deep_repository") return "deep"; + if (mode === "repository") return "standard"; + return "unknown"; +} + +function renderLocation( + target: ScanTargetRecord, + location: FindingLocation, +): string { + const role = + location.role === undefined ? "Location" : humanizeRole(location.role); + const label = `${location.path}:${location.startLine}${ + location.endLine === undefined || location.endLine === location.startLine + ? "" + : `-${location.endLine}` + }`; + const sourceUrl = immutableSourceUrl(target, location); + return `- **${role}:** ${sourceUrl === undefined ? `\`${label}\`` : `[\`${label}\`](${sourceUrl})`}`; +} + +function renderCodeEvidence( + target: ScanTargetRecord, + evidence: FindingCodeEvidence, +): string[] { + const location: FindingLocation = { + path: evidence.path, + startLine: evidence.startLine, + ...(evidence.endLine === undefined ? {} : { endLine: evidence.endLine }), + ...(evidence.role === undefined ? {} : { role: evidence.role }), + }; + return [ + `### ${evidence.label}`, + "", + renderLocation(target, location), + "", + fencedCode(evidence.code, evidence.language), + "", + evidence.explanation, + ]; +} + +function humanizeRole(role: string): string { + const words = role.replaceAll("_", " "); + return `${words.slice(0, 1).toUpperCase()}${words.slice(1)}`; +} + +function fencedCode(code: string, language?: string): string { + let fenceLength = 3; + for (const match of code.matchAll(/`+/g)) { + fenceLength = Math.max(fenceLength, match[0].length + 1); + } + const fence = "`".repeat(fenceLength); + const tag = + language !== undefined && /^[A-Za-z0-9_+.-]+$/.test(language) + ? language + : ""; + return `${fence}${tag}\n${code}\n${fence}`; +} + +function immutableSourceUrl( + target: ScanTargetRecord, + location: FindingLocation, +): string | undefined { + if ( + target.kind !== "git_revision" || + target.remote === undefined || + target.revision === undefined || + !/^(?:[a-f0-9]{40}|[a-f0-9]{64})$/i.test(target.revision) || + !isSafeRepositoryPath(location.path) + ) { + return undefined; + } + + let remote: URL; + try { + remote = new URL(target.remote); + } catch { + return undefined; + } + if ( + remote.protocol !== "https:" || + (remote.hostname !== "github.com" && !remote.hostname.endsWith(".ghe.com")) + ) { + return undefined; + } + + const repository = remote.pathname + .replace(/\.git\/?$/, "") + .replace(/\/$/, ""); + const path = location.path.split("/").map(encodeURIComponent).join("/"); + remote.pathname = `${repository}/blob/${target.revision}/${path}`; + remote.hash = `L${location.startLine}${ + location.endLine === undefined || location.endLine === location.startLine + ? "" + : `-L${location.endLine}` + }`; + return remote.toString(); +} + +function isSafeRepositoryPath(path: string): boolean { + if ( + path.includes("\\") || + /^[A-Za-z]:/.test(path) || + /[\u0000-\u001f\u007f]/.test(path) + ) { + return false; + } + + return path + .split("/") + .every( + (segment) => + segment.length > 0 && + segment !== "." && + segment !== ".." && + !/%(?:2e|2f|5c)/i.test(segment), + ); +} diff --git a/sdk/typescript/tests-ts/publication.test.ts b/sdk/typescript/tests-ts/publication.test.ts new file mode 100644 index 00000000..c5a3df31 --- /dev/null +++ b/sdk/typescript/tests-ts/publication.test.ts @@ -0,0 +1,330 @@ +import { createHash } from "node:crypto"; +import { chmod, cp, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, describe, expect, test } from "bun:test"; +import { prepareScanPublication } from "../src/publication.js"; +import type { + FindingsDocument, + ScanManifest, + SeverityLevel, +} from "../src/models.js"; +import { PLUGIN_ROOT } from "./plugin-root.js"; + +const EXAMPLE = join(PLUGIN_ROOT, "examples", "completed-scan"); +const temporaryDirectories: string[] = []; +const DESTINATION = { + destination: "linear", + teamId: "team_example", + projectId: "project_example", + uploadedAt: "2026-06-01T10:30:00Z", +} as const; + +afterEach(async () => { + await Promise.all( + temporaryDirectories + .splice(0) + .map((directory) => rm(directory, { recursive: true, force: true })), + ); +}); + +async function copyExample(): Promise { + const root = await mkdtemp(join(tmpdir(), "codex-security-publication-")); + temporaryDirectories.push(root); + const scanDirectory = join(root, "scan"); + await cp(EXAMPLE, scanDirectory, { recursive: true }); + if (process.platform !== "win32") await chmod(scanDirectory, 0o700); + return scanDirectory; +} + +async function readJson(path: string): Promise { + return JSON.parse(await readFile(path, "utf8")) as T; +} + +async function writeJson(path: string, value: unknown): Promise { + await writeFile(path, `${JSON.stringify(value, null, 2)}\n`); +} + +async function reseal(scanDirectory: string): Promise { + const manifestPath = join(scanDirectory, "scan-manifest.json"); + const manifest = await readJson(manifestPath); + for (const artifact of manifest.scan.artifacts) { + artifact.sha256 = createHash("sha256") + .update(await readFile(join(scanDirectory, artifact.path))) + .digest("hex"); + } + await writeJson(manifestPath, manifest); +} + +describe("scan publication preparation", () => { + test("prepares sealed findings with scan-based upload IDs and full traceability", async () => { + const scanDirectory = await copyExample(); + const publication = await prepareScanPublication( + scanDirectory, + DESTINATION, + ); + + expect(publication).toMatchObject({ + scanId: "scan_example_001", + uploadId: "scan_example_001", + scanDirectory, + destination: { + type: "linear", + teamId: "team_example", + projectId: "project_example", + }, + issues: [ + { + findingId: "csf_852f90d6e1177502ff113d4a", + occurrenceId: "occ_e79cb19591e696572a1c22be", + title: + "[Codex Security][HIGH] Unsafe archive extraction can escape the output directory", + priority: 2, + }, + ], + }); + + const issue = publication.issues[0]!; + expect(issue.title).not.toContain(publication.scanId); + expect(issue.title).not.toContain("example/repo"); + expect(issue.description).toContain("**Scan ID:** scan_example_001"); + expect(issue.description).toContain("**Upload ID:** scan_example_001"); + expect(issue.description).toContain(issue.findingId); + expect(issue.description).toContain(issue.occurrenceId); + expect(issue.description).toContain("**Repository:** example/repo"); + expect(issue.description).toContain("https://github.com/example/repo"); + expect(issue.description).toContain("**Base revision:** deadbeef"); + expect(issue.description).toContain( + "**Snapshot digest:** codex-security-snapshot/v1:sha256:", + ); + expect(issue.description).toContain("**Scanned scope:** ."); + expect(issue.description).toContain("**Coverage:** complete"); + expect(issue.description).toContain("**Scan mode:** standard"); + expect(issue.description).toContain("**CWE:** CWE-22"); + expect(issue.description).toContain("**Sink:** `src/extract.py:41-44`"); + expect(issue.description).toContain("**Uploaded:** 2026-06-01T10:30:00Z"); + expect(issue.description).toContain("without containment validation"); + expect(issue.description).toContain("Normalize destinations"); + expect(issue.description).not.toContain("/blob/deadbeef/"); + }); + + test("includes every canonical source snippet, location role, and root-cause code", async () => { + const scanDirectory = await copyExample(); + const findingsPath = join(scanDirectory, "findings.json"); + const findings = await readJson(findingsPath); + const finding = findings.findings[0]!; + finding.locations.push({ + path: "src/archive.py", + startLine: 12, + role: "root_control", + }); + finding.codeEvidence = [ + { + id: "untrusted-source", + label: "Untrusted archive entry", + path: "src/archive.py", + startLine: 12, + language: "python", + role: "source", + code: "entry = archive.read(request.path)", + explanation: "An attacker controls the selected entry.", + }, + { + id: "filesystem-sink", + label: "Filesystem write", + path: "src/extract.py", + startLine: 41, + endLine: 44, + language: "python", + role: "sink", + code: "destination.write_bytes(entry.read())", + explanation: "No containment validation runs before the write.", + }, + { + id: "markdown-fence", + label: "Literal Markdown delimiter", + path: "src/extract.py", + startLine: 43, + language: "python\n## unexpected-heading", + code: "````\nprint('literal fence')", + explanation: "Source text can contain Markdown fence characters.", + }, + ]; + finding.rootCause = { + summary: "Archive paths bypass containment validation.", + language: "python", + code: "destination = output / entry.name", + }; + await writeJson(findingsPath, findings); + await reseal(scanDirectory); + + const { description } = ( + await prepareScanPublication(scanDirectory, DESTINATION) + ).issues[0]!; + + expect(description).toContain("**Root control:** `src/archive.py:12`"); + expect(description).toContain( + "Archive paths bypass containment validation.", + ); + expect(description).toContain( + "```python\ndestination = output / entry.name\n```", + ); + expect(description).toContain("### Untrusted archive entry"); + expect(description).toContain("**Source:** `src/archive.py:12`"); + expect(description).toContain( + "```python\nentry = archive.read(request.path)\n```", + ); + expect(description).toContain("An attacker controls the selected entry."); + expect(description).toContain("### Filesystem write"); + expect(description).toContain( + "```python\ndestination.write_bytes(entry.read())\n```", + ); + expect(description).toContain( + "No containment validation runs before the write.", + ); + expect(description).toContain("`````\n````\nprint('literal fence')\n`````"); + expect(description).not.toContain("unexpected-heading"); + }); + + test("only links source locations for a full immutable GitHub revision", async () => { + const scanDirectory = await copyExample(); + const manifestPath = join(scanDirectory, "scan-manifest.json"); + const manifest = await readJson(manifestPath); + manifest.scan.target.kind = "git_revision"; + manifest.scan.target.revision = "0123456789abcdef0123456789abcdef01234567"; + delete manifest.scan.target.snapshotDigest; + await writeJson(manifestPath, manifest); + + const { description } = ( + await prepareScanPublication(scanDirectory, DESTINATION) + ).issues[0]!; + + expect(description).toContain( + "https://github.com/example/repo/blob/0123456789abcdef0123456789abcdef01234567/src/extract.py#L41-L44", + ); + expect(description).toContain( + "**Revision:** 0123456789abcdef0123456789abcdef01234567", + ); + expect(description).not.toContain("Snapshot digest"); + }); + + test("does not turn non-HTTPS repository remotes into source links", async () => { + const scanDirectory = await copyExample(); + const manifestPath = join(scanDirectory, "scan-manifest.json"); + const manifest = await readJson(manifestPath); + manifest.scan.target.kind = "git_revision"; + manifest.scan.target.revision = "0123456789abcdef0123456789abcdef01234567"; + manifest.scan.target.remote = "ssh://github.com/example/repo"; + delete manifest.scan.target.snapshotDigest; + await writeJson(manifestPath, manifest); + + const { description } = ( + await prepareScanPublication(scanDirectory, DESTINATION) + ).issues[0]!; + + expect(description).toContain("**Remote:** ssh://github.com/example/repo"); + expect(description).toContain("**Sink:** `src/extract.py:41-44`"); + expect(description).not.toContain("/blob/"); + }); + + test("preserves unsafe evidence snippets without generating escaping source links", async () => { + const scanDirectory = await copyExample(); + const manifestPath = join(scanDirectory, "scan-manifest.json"); + const manifest = await readJson(manifestPath); + manifest.scan.target.kind = "git_revision"; + manifest.scan.target.revision = "0123456789abcdef0123456789abcdef01234567"; + delete manifest.scan.target.snapshotDigest; + await writeJson(manifestPath, manifest); + + const findingsPath = join(scanDirectory, "findings.json"); + const findings = await readJson(findingsPath); + const paths = [ + "../outside.py", + "src/../outside.py", + "src/./outside.py", + "src//outside.py", + "/outside.py", + "src\\outside.py", + "C:/outside.py", + "src/%2e%2e/outside.py", + "src/%2foutside.py", + "src/%5coutside.py", + ]; + findings.findings[0]!.codeEvidence = paths.map((path, index) => ({ + id: `unsafe-path-${index}`, + label: `Unsafe path ${index}`, + path, + startLine: 41, + role: "source", + code: `preserved_snippet_${index}()`, + explanation: + "Preserve canonical evidence even without a safe source link.", + })); + await writeJson(findingsPath, findings); + await reseal(scanDirectory); + + const { description } = ( + await prepareScanPublication(scanDirectory, DESTINATION) + ).issues[0]!; + + expect(description).toContain( + "https://github.com/example/repo/blob/0123456789abcdef0123456789abcdef01234567/src/extract.py#L41-L44", + ); + for (const [index, path] of paths.entries()) { + expect(description).toContain(`**Source:** \`${path}:41\``); + expect(description).toContain(`preserved_snippet_${index}()`); + expect(description).not.toContain(`[\`${path}:41\`](`); + } + }); + + test.each([ + ["critical", 1], + ["high", 2], + ["medium", 3], + ["low", 4], + ["informational", undefined], + ] as const)( + "maps %s severity to Linear priority %s", + async (severity, priority) => { + const scanDirectory = await copyExample(); + const findingsPath = join(scanDirectory, "findings.json"); + const findings = await readJson(findingsPath); + findings.findings[0]!.severity.level = severity satisfies SeverityLevel; + await writeJson(findingsPath, findings); + await reseal(scanDirectory); + + const issue = (await prepareScanPublication(scanDirectory, DESTINATION)) + .issues[0]!; + expect(issue.title).toStartWith( + `[Codex Security][${severity.toUpperCase()}] `, + ); + expect(issue.priority).toBe(priority); + if (priority === undefined) expect(issue).not.toHaveProperty("priority"); + }, + ); + + test("preserves an empty sealed finding set", async () => { + const scanDirectory = await copyExample(); + const findingsPath = join(scanDirectory, "findings.json"); + const findings = await readJson(findingsPath); + findings.findings = []; + await writeJson(findingsPath, findings); + await reseal(scanDirectory); + + expect( + (await prepareScanPublication(scanDirectory, DESTINATION)).issues, + ).toEqual([]); + }); + + test("rejects findings whose sealed artifact has been modified", async () => { + const scanDirectory = await copyExample(); + const findingsPath = join(scanDirectory, "findings.json"); + const findings = await readJson(findingsPath); + findings.findings[0]!.summary = "Modified after the scan was sealed."; + await writeJson(findingsPath, findings); + + await expect( + prepareScanPublication(scanDirectory, DESTINATION), + ).rejects.toThrow(); + }); +}); From 4822111857745ea8d3af3a0d745a5de416b84f7c Mon Sep 17 00:00:00 2001 From: Kyle Brown Date: Sat, 15 Aug 2026 08:04:53 +0000 Subject: [PATCH 2/4] fix(sdk): avoid redundant upload identifiers in findings --- sdk/typescript/src/publication.ts | 1 - sdk/typescript/tests-ts/publication.test.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/sdk/typescript/src/publication.ts b/sdk/typescript/src/publication.ts index d6bc8334..41616f8c 100644 --- a/sdk/typescript/src/publication.ts +++ b/sdk/typescript/src/publication.ts @@ -89,7 +89,6 @@ function renderFindingDescription( "## Codex Security finding", "", `**Scan ID:** ${scan.id}`, - `**Upload ID:** ${scan.id}`, `**Finding ID:** ${finding.findingId}`, `**Occurrence ID:** ${finding.occurrenceId}`, `**Fingerprint:** ${finding.fingerprints.primary}`, diff --git a/sdk/typescript/tests-ts/publication.test.ts b/sdk/typescript/tests-ts/publication.test.ts index c5a3df31..1991f2e6 100644 --- a/sdk/typescript/tests-ts/publication.test.ts +++ b/sdk/typescript/tests-ts/publication.test.ts @@ -88,7 +88,7 @@ describe("scan publication preparation", () => { expect(issue.title).not.toContain(publication.scanId); expect(issue.title).not.toContain("example/repo"); expect(issue.description).toContain("**Scan ID:** scan_example_001"); - expect(issue.description).toContain("**Upload ID:** scan_example_001"); + expect(issue.description).not.toContain("**Upload ID:**"); expect(issue.description).toContain(issue.findingId); expect(issue.description).toContain(issue.occurrenceId); expect(issue.description).toContain("**Repository:** example/repo"); From 5b18c90fda6f4d521dbb1f0ddd1198d71684e80e Mon Sep 17 00:00:00 2001 From: Kyle Brown Date: Sat, 15 Aug 2026 22:47:38 +0000 Subject: [PATCH 3/4] feat(publish): allow team-only Linear publication destinations --- sdk/typescript/src/publication.ts | 8 +++++--- sdk/typescript/tests-ts/publication.test.ts | 19 +++++++++++++++++++ 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/sdk/typescript/src/publication.ts b/sdk/typescript/src/publication.ts index 41616f8c..286eed66 100644 --- a/sdk/typescript/src/publication.ts +++ b/sdk/typescript/src/publication.ts @@ -12,13 +12,13 @@ import { bundledPluginRoot } from "./runtime.js"; export interface LinearPublicationDestination { type: "linear"; teamId: string; - projectId: string; + projectId?: string; } export interface PrepareScanPublicationOptions { destination: "linear"; teamId: string; - projectId: string; + projectId?: string; uploadedAt?: string; } @@ -63,7 +63,9 @@ export async function prepareScanPublication( destination: { type: options.destination, teamId: options.teamId, - projectId: options.projectId, + ...(options.projectId === undefined + ? {} + : { projectId: options.projectId }), }, issues: contract.findings.findings.map((finding) => { const priority = LINEAR_PRIORITIES[finding.severity.level]; diff --git a/sdk/typescript/tests-ts/publication.test.ts b/sdk/typescript/tests-ts/publication.test.ts index 1991f2e6..1fae32d0 100644 --- a/sdk/typescript/tests-ts/publication.test.ts +++ b/sdk/typescript/tests-ts/publication.test.ts @@ -108,6 +108,25 @@ describe("scan publication preparation", () => { expect(issue.description).not.toContain("/blob/deadbeef/"); }); + test("prepares sealed findings for a Linear team without a project", async () => { + const scanDirectory = await copyExample(); + const publication = await prepareScanPublication(scanDirectory, { + destination: "linear", + teamId: "team_example", + uploadedAt: "2026-06-01T10:30:00Z", + }); + + expect(publication.destination).toEqual({ + type: "linear", + teamId: "team_example", + }); + expect(publication.destination).not.toHaveProperty("projectId"); + expect(publication.issues[0]).toMatchObject({ + findingId: "csf_852f90d6e1177502ff113d4a", + occurrenceId: "occ_e79cb19591e696572a1c22be", + }); + }); + test("includes every canonical source snippet, location role, and root-cause code", async () => { const scanDirectory = await copyExample(); const findingsPath = join(scanDirectory, "findings.json"); From 8ace47083128a51dda68ca52fd13e19c3e672c1d Mon Sep 17 00:00:00 2001 From: Kyle Brown Date: Sun, 16 Aug 2026 00:10:57 +0000 Subject: [PATCH 4/4] fix(publish): preserve every sealed scan coverage mode --- sdk/typescript/src/publication.ts | 2 +- sdk/typescript/tests-ts/publication.test.ts | 29 +++++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/sdk/typescript/src/publication.ts b/sdk/typescript/src/publication.ts index 286eed66..f4255a0c 100644 --- a/sdk/typescript/src/publication.ts +++ b/sdk/typescript/src/publication.ts @@ -171,7 +171,7 @@ function renderTargetIdentity(target: ScanTargetRecord): string[] { function scanMode(mode: LoadedContract["coverage"]["mode"]): string { if (mode === "deep_repository") return "deep"; if (mode === "repository") return "standard"; - return "unknown"; + return mode; } function renderLocation( diff --git a/sdk/typescript/tests-ts/publication.test.ts b/sdk/typescript/tests-ts/publication.test.ts index 1fae32d0..15d61526 100644 --- a/sdk/typescript/tests-ts/publication.test.ts +++ b/sdk/typescript/tests-ts/publication.test.ts @@ -5,6 +5,7 @@ import { join } from "node:path"; import { afterEach, describe, expect, test } from "bun:test"; import { prepareScanPublication } from "../src/publication.js"; import type { + CoverageDocument, FindingsDocument, ScanManifest, SeverityLevel, @@ -108,6 +109,34 @@ describe("scan publication preparation", () => { expect(issue.description).not.toContain("/blob/deadbeef/"); }); + test.each([ + ["repository", "standard"], + ["scoped_path", "scoped_path"], + ["diff", "diff"], + ["commit", "commit"], + ["branch_diff", "branch_diff"], + ["working_tree", "working_tree"], + ["deep_repository", "deep"], + ] as const)( + "preserves truthful scan provenance for %s coverage", + async (mode, expectedMode) => { + const scanDirectory = await copyExample(); + const coveragePath = join(scanDirectory, "coverage.json"); + const coverage = await readJson(coveragePath); + coverage.mode = mode; + await writeJson(coveragePath, coverage); + await reseal(scanDirectory); + + const { description } = ( + await prepareScanPublication(scanDirectory, DESTINATION) + ).issues[0]!; + + expect(description).toContain(`**Coverage mode:** ${mode}`); + expect(description).toContain(`**Scan mode:** ${expectedMode}`); + expect(description).not.toContain("**Scan mode:** unknown"); + }, + ); + test("prepares sealed findings for a Linear team without a project", async () => { const scanDirectory = await copyExample(); const publication = await prepareScanPublication(scanDirectory, {