Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions sdk/typescript/scripts/check-package.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ const distFiles = new Set(
"knowledge-base",
"models",
"multiscan",
"publication",
"result",
"runtime",
"scan-activity",
Expand Down
289 changes: 289 additions & 0 deletions sdk/typescript/src/publication.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,289 @@
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<SeverityLevel, 1 | 2 | 3 | 4 | undefined>;

export async function prepareScanPublication(
scanDirectory: string,
options: PrepareScanPublicationOptions,
): Promise<PreparedScanPublication> {
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,
...(options.projectId === undefined
? {}
: { 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}`,
`**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 mode;
}

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),
);
}
Loading
Loading