Skip to content
Draft
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
8 changes: 4 additions & 4 deletions plugins/codex-security/skills/publish-findings/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: publish-findings
description: Import selected Wiz findings, GitHub code-scanning alerts, or GHSA security advisories to Codex Security Cloud after previewing the destination and evidence. Supports saved JSON/JSONL and gzip; does not run a scan, assess applicability, or synchronize triage decisions.
description: Import selected Wiz findings, GitHub code-scanning alerts, or GHSA security advisories to Codex Security Cloud after previewing the destination and evidence. Supports saved JSON/JSONL, Wiz Code repository vulnerability CSV, and gzip; does not run a scan, assess applicability, or synchronize triage decisions.
---

# Publish external findings
Expand All @@ -11,15 +11,15 @@ Cloud destinations currently support connected GitHub and GitHub Enterprise repo

## Wiz findings

Read selected Wiz findings through the user's configured connection or supplied export. Save the actual selected JSON records locally, retaining their IDs and evidence. Finish the requested pagination; if the user selected a subset, save that explicit subset and retain its named collection. The command does not fetch from Wiz or accept CSV, scan events, workload secrets, cloud configuration, or external network exports. The publisher also accepts JSONL records with `source_finding_id` and `evidence` as described in the CLI documentation.
Read selected Wiz findings through the user's configured connection or supplied export. Save the actual selected records locally, retaining their IDs and evidence. Finish the requested pagination; if the user selected a subset, save that explicit subset and retain its named collection. The command accepts Wiz Code repository package-vulnerability CSV directly, as well as JSON and normalized JSONL records with `source_finding_id` and `evidence` as described in the CLI documentation. It does not fetch from Wiz or accept other CSV profiles, scan events, workload secrets, cloud configuration, or external network exports.

Use a stable Wiz tenant/finding namespace as `--source-key`. A project filter is selection context, not a new identity. Confirm the selected assets or builds belong to the destination repository; do not infer a scanner branch or revision from the current checkout. Do not publish vendor credentials, unrelated records, or local assessments as original scanner evidence.

For package vulnerability UI exports, use **Vulnerability Findings → Save as → Report**, keeping the repository and finding filters. Select **Repository Branch** (remove the default Virtual Machine selection), **JSON**, and **Detailed** columns. Download the completed report; default gzip compression is supported, even with a `.json` filename. **Raw Event** on Code & Build Scans contains scan metadata and cannot supply findings. Do not create a scheduled report or enable notifications or external storage unless requested.
For package vulnerability UI exports, use **Vulnerability Findings → Save as → Report**, keeping the repository and finding filters. Select **Repository Branch** (remove the default Virtual Machine selection), **JSON** or comma-separated **CSV**, and **Detailed** columns. Pass supported CSV reports directly to the command; their GitHub `CloudProviderURL` must identify the destination repository branch. The importer retains every original cell in `source_data.row` and uses the reported `ID` as occurrence identity. Download the completed report; gzip compression is supported regardless of the filename extension. **Raw Event** on Code & Build Scans contains scan metadata and cannot supply findings. Do not create a scheduled report or enable notifications or external storage unless requested.

For SAST, repository secrets, and IaC, retain the explicit `data.sastFindings.nodes`, `data.secretInstances.nodes`, or `data.iacFindings.nodes` collection. Bare arrays cannot identify these finding families. Include a verified URL on the source repository metadata or supply `data.versionControlResources.nodes` with matching `repository.id` and `repository.url`. Retain the documented inventory `platform` field; explicit `GITHUB` establishes case-insensitive matching on custom GitHub Enterprise hosts. Resolve conflicting platform or URL metadata for a repository ID before publishing. For secrets, the repository is `resource.typedProperties.repository` and resource type must be `REPOSITORY_BRANCH`. Do not map names, inventory node IDs, `providerID`, or Wiz UUIDs directly to Cloud repository IDs. The importer verifies source URLs against the selected destination; resolve a missing or conflicting mapping before uploading.

Keep raw source records unchanged in `source_data`. New raw occurrence IDs are prefixed `sast:`, `secret:`, or `iac:`; existing package IDs are unchanged. For CSV report conversion, use a reviewed mapping of the actual header names into the documented normalized JSONL profile, set `evidence.details.kind` and the verified `details.repository.url`, and use the same prefixed occurrence ID. The command does not parse CSV directly. Select source paths and line numbers explicitly in secret reports; the standard metadata query lacks them. Preserve rule/CWE, range/snippet, detector validation, and IaC comparison fields in the typed details described in the CLI docs. Preserve empty comparison strings. Do not invent missing IDs, severity, revisions, or locations, retrieve credential values, test credentials, or apply suggested fixes. Vendor status, AI verdict, secret validation, introduced commits, and logical IaC resources are evidence, not Codex assessment or proof of runtime deployment.
Keep raw source records unchanged in `source_data`. New raw occurrence IDs are prefixed `sast:`, `secret:`, or `iac:`; existing package IDs are unchanged. SAST, repository-secret, and IaC CSV reports still require a reviewed mapping of the actual header names into the documented normalized JSONL profile: set `evidence.details.kind` and the verified `details.repository.url`, and use the same prefixed occurrence ID. Select source paths and line numbers explicitly in secret reports; the standard metadata query lacks them. Preserve rule/CWE, range/snippet, detector validation, and IaC comparison fields in the typed details described in the CLI docs. Preserve empty comparison strings. Do not invent missing IDs, severity, revisions, or locations, retrieve credential values, test credentials, or apply suggested fixes. Vendor status, AI verdict, secret validation, introduced commits, and logical IaC resources are evidence, not Codex assessment or proof of runtime deployment.

## GitHub alerts and advisories

Expand Down
34 changes: 27 additions & 7 deletions sdk/typescript/docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -1169,11 +1169,12 @@ Before the first import:
authorized environment, but does not need a completed native scan.
2. For **package vulnerabilities**, in Wiz **Vulnerability Findings**, filter to the intended repository and
findings. Choose **Save as → Report**, select **Repository Branch** as the
resource type (remove the default Virtual Machine selection), **JSON** format,
resource type (remove the default Virtual Machine selection), **CSV** or **JSON** format,
and **Detailed** columns. Retain the repository filter and download the completed
report. Gzip compression can stay enabled; the importer detects it even when
the download is named `.json`. **Raw Event** on Code & Build Scans exports scan
metadata, not finding records. CSV is not supported.
metadata, not finding records. CSV requires GitHub repository-branch findings
and a comma delimiter; other report profiles need an explicit mapping.
Alternatively, read selected package vulnerability records through your
authorized Wiz connection and save their JSON. Include `id`, `name`,
`detailedName`, `vendorSeverity` (or `severity`), and `vulnerableAsset.id`;
Expand Down Expand Up @@ -1211,7 +1212,7 @@ the manual navigation guidance rather than guessing a UI host or port.
Preview the selection and destination:

```bash
codex-security publish findings selected-wiz.json \
codex-security publish findings selected-wiz.csv \
--to cloud --repository https://github.com/example/project --provider wiz \
--source-key TENANT_ID/vulnerability-finding --dry-run --format json
```
Expand All @@ -1233,12 +1234,31 @@ exclusions. `--dry-run --format json` retains all normalized findings and comple
evidence for inspection. A prompt explicitly includes any records that will be
skipped.

Package input can be a downloaded Wiz JSON vulnerability report, a vulnerability
Package input can be a downloaded Wiz Code vulnerability CSV, a Wiz JSON vulnerability report, a vulnerability
finding, an array, a complete `data.vulnerabilityFindings.nodes` response, or JSONL
with one package record per line. Plain and gzip-compressed files are accepted.
A response that advertises another page is rejected. Complete the requested pages
or save only the explicitly selected records; retain the named collection for
SAST, repository secrets, and IaC. Normalized JSONL is also accepted:
SAST, repository secrets, and IaC.

The supported CSV profile includes `ID`, `Name`, `DetailedName`, `AssetID`,
`WizURL`, `CloudPlatform`, and `CloudProviderURL`. Its public column conventions
are illustrated by the [Wiz vulnerability CSV fixture](https://github.com/DefectDojo/django-DefectDojo/blob/10453d6c62d68eee6d6b0ecc0acde5d61d1dd02d/unittests/scans/wiz/sca_format.csv).
For repository imports, `CloudPlatform` must identify GitHub and
`CloudProviderURL` must identify a repository branch. The repository URL must
match the selected Cloud repository, including when the parsed findings are
saved and read again as normalized JSONL. Other resource types are excluded.

CSV imports use `ID` as the occurrence identity and retain every parsed field,
including unknown columns and quoted multiline values, in `source_data.row`.
Severity comes from `Severity`, then `VendorSeverity`, then `CVSSSeverity`.
Vendor resolution status stays in the original evidence. The importer does not
derive a branch or revision from a tree URL, or treat last-detected time as a
record-update timestamp. Duplicate headers, damaged CSV rows, invalid UTF-8,
and duplicate occurrence IDs stop the import. Review any excluded records in
the preview before publishing.

Normalized JSONL is also accepted:

```json
{
Expand All @@ -1251,7 +1271,7 @@ SAST, repository secrets, and IaC. Normalized JSONL is also accepted:
}
```

Use Wiz's finding `id`, not a CVE, as identity. Keep the source key stable across
Use Wiz's finding `id` (`ID` in CSV), not a CVE, as identity. Keep the source key stable across
exports; a selected project filter does not change the vendor namespace. Confirm
that the selected assets or builds map to this repository. The raw Wiz adapter
retains each record in `source_data`, uses reported severity, and leaves unknown
Expand Down Expand Up @@ -1339,7 +1359,7 @@ does not establish that it is deployed. Precise original timestamps remain in
timestamps.

Native SAST and secret report downloads can be CSV, including gzip-compressed
CSV. CSV is not parsed directly. Convert a selected report with a reviewed mapping
CSV. Those CSV profiles are not parsed directly. Convert a selected report with a reviewed mapping
for its actual column headers into normalized JSONL, preserving each original row
in `source_data`. Set `details.kind` and `details.repository.url` explicitly after
verifying the source repository. Use the same prefixed occurrence ID shown above
Expand Down
1 change: 1 addition & 0 deletions sdk/typescript/scripts/package-dist-files.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ const distModules = [
"scanner-import",
"semgrep-findings",
"snyk-findings",
"wiz-csv",
"wiz-findings",
"codex-prompt",
"codex-sdk-environment",
Expand Down
4 changes: 2 additions & 2 deletions sdk/typescript/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3415,14 +3415,14 @@ export async function main(
publication.command("findings", {
description:
"Preview and import selected Wiz, GitHub, Snyk, Endor, or Semgrep SAST findings to Cloud.",
hint: "For Semgrep SAST, supply JSON or SARIF and --source-repository-url identifying the source repository. For Endor, supply native Finding JSON or endorctl SARIF 2.1.0. For Snyk, supply self-contained Open Source SARIF 2.1.0. For GitHub, supply unfiltered JSON/JSONL saved by import github or import github-advisories. For Wiz, supply a package vulnerability JSON report, named SAST/repository-secret/IaC JSON collections with repository metadata, or normalized JSONL. Gzip is supported; CSV, scan events, workload secrets, and cloud configuration exports are not. --full-output requires --format json or --format jsonl.\nExample: codex-security publish findings selected-wiz.json --to cloud --repository https://github.com/example/project --provider wiz --source-key TENANT_ID/vulnerability-finding --dry-run --format json",
hint: "For Semgrep SAST, supply JSON or SARIF and --source-repository-url identifying the source repository. For Endor, supply native Finding JSON or endorctl SARIF 2.1.0. For Snyk, supply self-contained Open Source SARIF 2.1.0. For GitHub, supply unfiltered JSON/JSONL saved by import github or import github-advisories. For Wiz, supply a Wiz Code vulnerability CSV for GitHub repositories, a package vulnerability JSON report, named SAST/repository-secret/IaC JSON collections with repository metadata, or normalized JSONL. Gzip is supported; other CSV profiles, scan events, workload secrets, and cloud configuration exports are not. --full-output requires --format json or --format jsonl.\nExample: codex-security publish findings selected-wiz.json --to cloud --repository https://github.com/example/project --provider wiz --source-key TENANT_ID/vulnerability-finding --dry-run --format json",
destructive: true,
mcp: false,
args: z.object({
file: z
.string()
.describe(
"Semgrep SAST or Endor JSON/SARIF, Snyk Open Source SARIF, saved GitHub import output, Wiz finding collections, or normalized Wiz JSONL.",
"Semgrep SAST or Endor JSON/SARIF, Snyk Open Source SARIF, saved GitHub import output, Wiz Code vulnerability CSV, Wiz finding collections, or normalized Wiz JSONL.",
),
}),
options: z.object({
Expand Down
4 changes: 4 additions & 0 deletions sdk/typescript/src/external-findings-publish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
validatePreparedOutputDir,
} from "./runtime.js";
import { readVendorFindingsForPublication } from "./wiz-findings.js";
import { wizCsvRepositoryUrl } from "./wiz-csv.js";
import { readGitHubFindings } from "./github-findings.js";
import { readSnykFindings, snykRepositoryUrls } from "./snyk-findings.js";
import { readSemgrepFindings } from "./semgrep-findings.js";
Expand Down Expand Up @@ -723,6 +724,9 @@ async function preparePublicationInput(
for (const finding of parsed.findings) {
const sourceRepositories = [
finding.evidence.details?.repository?.url,
...(options.provider === "wiz"
? [wizCsvRepositoryUrl(finding.evidence)]
: []),
...(options.provider === "snyk"
? snykRepositoryUrls(finding.evidence.source_data)
: []),
Expand Down
136 changes: 136 additions & 0 deletions sdk/typescript/src/wiz-csv.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
import Papa from "papaparse";
import { validateExternalEvidence } from "./external-import-contract.js";
import type { ExternalFindingEvidence } from "./external-import-models.js";
import type { VendorFinding } from "./wiz-findings.js";

const profile = "wiz-code-vulnerability-csv";
const requiredHeaders = [
"ID",
"Name",
"DetailedName",
"AssetID",
"WizURL",
"CloudPlatform",
"CloudProviderURL",
];

function text(value: unknown): string | null {
return typeof value === "string" && value.trim() ? value : null;
}

/** Recognize the public Wiz vulnerability CSV profile, retaining every cell. */
export function readWizCsv(
contents: string,
): Record<string, string>[] | undefined {
// JSONL can retain CSV column names in its evidence. Recognize the first
// JSON value before interpreting all of its quoted records as CSV.
try {
JSON.parse(contents.trimStart().split(/\r?\n/u, 1)[0]!);
return undefined;
} catch {
// A CSV header is not a JSON value.
}
// Read arrays so duplicate headers cannot be silently renamed, and unknown
// columns (including object-prototype names) remain ordinary source data.
const parsed = Papa.parse<string[]>(contents, {
delimiter: ",",
skipEmptyLines: true,
});
const [headers, ...rows] = parsed.data;
if (!headers || !requiredHeaders.every((name) => headers.includes(name)))
return undefined;
if (parsed.errors.length)
throw new Error(`Invalid Wiz CSV: ${parsed.errors[0]!.message}`);
if (new Set(headers).size !== headers.length)
throw new Error("Wiz CSV contains duplicate column names.");
return rows.map((cells, index) => {
if (cells.length !== headers.length)
throw new Error(`Wiz CSV row ${index + 2} has the wrong column count.`);
return Object.fromEntries(headers.map((name, i) => [name, cells[i]!]));
});
}

function repositoryUrl(row: Record<string, unknown>): string {
if (text(row["CloudPlatform"])?.trim().toLowerCase() !== "github")
throw new Error(
"Wiz CSV imports require GitHub repository-branch findings. Workload and other provider CSV reports need an explicit normalized JSONL mapping.",
);
const value = text(row["CloudProviderURL"]);
const url = value ? new URL(value) : undefined;
const origin = value?.match(/^(https?:\/\/[^/?#]+)/iu)?.[1];
const path = url?.pathname.match(/^\/([^/]+)\/([^/]+)\/tree\/(.+)$/u);
if (
!url ||
!origin ||
!["http:", "https:"].includes(url.protocol) ||
url.username ||
url.password ||
!path
)
throw new Error("Wiz CSV finding requires a GitHub repository-branch URL.");
// URL.origin removes explicit default ports, which the Cloud contract keeps.
return `${origin}/${path[1]}/${path[2]}`;
}

/** Keep repository binding available after a normalized JSONL round trip. */
export function wizCsvRepositoryUrl(
evidence: ExternalFindingEvidence,
): string | undefined {
const source = evidence.source_data;
if (source?.["format"] !== profile) return undefined;
const row = source["row"];
if (!row || typeof row !== "object" || Array.isArray(row))
throw new Error("Wiz CSV source evidence is missing its original row.");
return repositoryUrl(row as Record<string, unknown>);
}

export function wizCsvFinding(
row: Record<string, unknown>,
sourceFindingId: string,
): VendorFinding {
repositoryUrl(row);
const name = text(row["Name"]);
const packageName = text(row["DetailedName"]);
if (!name || !packageName || !text(row["AssetID"]))
throw new Error(
"Wiz CSV finding requires Name, DetailedName, and AssetID.",
);
const path = text(text(row["LocationPath"])?.replace(/^\/+/, ""));
const fixedVersion = text(row["FixedVersion"]);
const severity = (
text(row["Severity"]) ??
text(row["VendorSeverity"]) ??
text(row["CVSSSeverity"])
)
?.trim()
.toLowerCase();
return {
source_finding_id: sourceFindingId,
evidence: validateExternalEvidence({
title: [packageName, text(row["Version"]), name]
.filter(Boolean)
.join(" "),
description: text(row["Description"]),
severity,
url: text(row["WizURL"]),
advisory_ids: [name],
packages: [
{
name: packageName,
ecosystem: null,
installed_version: text(row["Version"]),
manifest_path: path,
fixed_versions: fixedVersion ? [fixedVersion] : [],
},
],
locations: path ? [{ path }] : [],
// A tree URL does not distinguish a slash-containing ref from a directory.
branch: null,
code_revision: null,
source_scan_id: null,
source_updated_at: null,
image_digests: [],
source_data: { format: profile, row },
}),
};
}
Loading
Loading