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
48 changes: 48 additions & 0 deletions docs/scanner-imports.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,3 +76,51 @@ Synthetic QA covers both formats and 500-record exports; live tenant verificatio
is separate.

Endor list responses with a continuation cursor or token are rejected. Finish fetching the export or provide an explicitly selected array; a single unfinished page is not a complete import. Fractional timestamps are rounded down to Cloud epoch seconds while their original precision remains in source evidence.

## Semgrep SAST

Use Semgrep JSON or self-contained SARIF 2.1.0, with an explicit declaration of
the source repository. The publisher checks this URL against the selected Cloud
repository; it never fills missing source provenance from the destination.

```sh
codex-security publish findings semgrep.json --to cloud \
--provider semgrep --source-key ORGANIZATION/PROJECT/BRANCH \
--source-repository-url https://github.com/example/project \
--repository https://github.com/example/project --dry-run --format json
```

Review the findings and exclusions, then repeat without `--dry-run` and confirm
the upload. Semgrep requires a Cloud version that accepts the `semgrep` provider.

- Scope: Semgrep SAST. Do not supply Supply Chain or Secrets exports; identifiable
records from these products are excluded. SARIF may omit product metadata, so
select a SAST-only scan when exporting it.
- Logged-in Platform fingerprints are stable across range and message changes.
JSON and SARIF reuse a matching fingerprint. Community Edition omits stable
fingerprints or emits the placeholder `requires login`; its fallback combines
the rule, literal path, and range. Moving a CE finding creates a new identity.
Keep a stable source key and review changed exports; this adapter cannot infer
cross-export equivalence for moved CE findings.
- SARIF supports direct artifact URIs and Semgrep's native `%SRCROOT%` marker
without a base definition. Native paths retain literal percent characters;
direct URIs without that marker are URI-decoded once.
Indexed artifacts, other URI bases and redefined
source roots remain in the original evidence without repository locations.
CE records with those references are excluded because a reliable fallback
identity cannot be derived. See the [native SARIF formatter](https://github.com/semgrep/semgrep/blob/51602ad27f09ef9342a5620601de5ef8f572fbc6/src/osemgrep/reporting/Sarif_output.ml#L410).
- Prefer JSON for original severity. Semgrep SARIF combines Critical and High
into diagnostic `error`, which this mapper displays as High. Both representations
retain the complete selected source evidence.
- Scan-wide JSON path inventories and timing tables are omitted from individual
findings; the selected raw result and remaining report metadata are retained.
- JSON scan errors, unsuccessful SARIF invocations, and SARIF scan-error
notifications are rejected. Ambiguous
duplicate identities and invalid records are excluded before publication.
- Original source suppressions and ignored status stay separate from Cloud
lifecycle state and Codex assessment. This adapter does not launch a scan,
evaluate rules, follow URLs, or read source paths from the report.

Programmatic callers pass `{ sourceRepositoryUrl }` to
`normalizeSemgrepFindings`, `normalizeSemgrepSarif`, or `readSemgrepFindings`.
See the [official output documentation](https://docs.semgrep.dev/semgrep-appsec-platform/json-and-sarif).
2 changes: 1 addition & 1 deletion sdk/typescript/schemas/external-findings.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@
"additionalProperties": false,
"properties": {
"provider": {
"enum": ["wiz", "endor", "snyk", "github"],
"enum": ["wiz", "endor", "snyk", "github", "semgrep"],
"title": "Provider",
"type": "string"
},
Expand Down
2 changes: 1 addition & 1 deletion sdk/typescript/scripts/fixtures/package-consumer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,7 @@ export async function externalPublication(
): Promise<ExternalPublicationResult> {
const fromFile = await prepareExternalPublication(file, options);
fromFile.preview.requests[0]?.source.provider satisfies
"wiz" | "endor" | "snyk" | "github" | undefined;
"wiz" | "endor" | "snyk" | "github" | "semgrep" | undefined;
const prepared = await prepareNormalizedExternalPublication(input, options);
return prepared.publish();
}
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 @@ -23,6 +23,7 @@ const distModules = [
"external-import-models",
"endor-findings",
"scanner-import",
"semgrep-findings",
"snyk-findings",
"wiz-findings",
"codex-prompt",
Expand Down
13 changes: 9 additions & 4 deletions sdk/typescript/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3255,15 +3255,15 @@ export async function main(
});
publication.command("findings", {
description:
"Preview and import selected Wiz, GitHub, Snyk, or Endor findings to Cloud.",
hint: "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",
"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",
destructive: true,
mcp: false,
args: z.object({
file: z
.string()
.describe(
"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 finding collections, or normalized Wiz JSONL.",
),
}),
options: z.object({
Expand All @@ -3272,8 +3272,13 @@ export async function main(
"Authorized Cloud repository ID or repository URL copied from Cloud.",
),
provider: z
.enum(["wiz", "github", "snyk", "endor"])
.enum(["wiz", "github", "snyk", "endor", "semgrep"])
.describe("Provider that produced these findings."),
sourceRepositoryUrl: optionValue("--source-repository-url")
.optional()
.describe(
"Required for Semgrep: declared source repository URL, verified against the Cloud destination.",
),
sourceKey: optionValue("--source-key").describe(
"Stable vendor tenant and finding namespace; not a project selection filter.",
),
Expand Down
20 changes: 12 additions & 8 deletions sdk/typescript/src/external-findings-publish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,14 @@ import {
import { readVendorFindingsForPublication } from "./wiz-findings.js";
import { readGitHubFindings } from "./github-findings.js";
import { readSnykFindings, snykRepositoryUrls } from "./snyk-findings.js";
import { readSemgrepFindings } from "./semgrep-findings.js";
import { readEndorFindings, endorRepositoryUrls } from "./endor-findings.js";
import type {
ExternalFindings,
ExternalPublicationInput,
} from "./external-findings.js";
import {
repositoryUrlKey,
cloudRepositoryUrlKey,
validateImportRequest,
validateExternalEvidence,
validateImportReceipt,
Expand All @@ -49,16 +50,11 @@ import type {

const MAX_REQUEST_BYTES = 5 * 1024 * 1024;

// Cloud's import catalog contains GitHub repositories, including custom Enterprise
// hosts. Match its case-insensitive URL identity without merging schemes or ports.
function cloudRepositoryUrlKey(value: string): string {
return repositoryUrlKey(value).toLowerCase();
}

/** CLI and plugin callers prepare the same immutable request before approval. */
export interface ExternalPublicationOptions {
repository: string;
provider: "wiz" | "github" | "snyk" | "endor";
provider: "wiz" | "github" | "snyk" | "endor" | "semgrep";
sourceRepositoryUrl?: string;
sourceKey: string;
}

Expand Down Expand Up @@ -289,6 +285,14 @@ export async function prepareExternalPublication(
options: ExternalPublicationOptions,
dependencies: ExternalPublicationDependencies = {},
): Promise<PreparedExternalPublication> {
if (options.provider === "semgrep")
return prepareNormalizedExternalPublication(
await readSemgrepFindings(path, {
sourceRepositoryUrl: options.sourceRepositoryUrl ?? "",
}),
options,
dependencies,
);
if (options.provider === "endor")
return prepareNormalizedExternalPublication(
await readEndorFindings(path),
Expand Down
6 changes: 6 additions & 0 deletions sdk/typescript/src/external-import-contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ export function repositoryUrlKey(value: string): string {
return `${parts[1]!.toLowerCase()}://${authority}${path}${parts[4]!}`;
}

// Cloud's import catalog contains GitHub repositories, including custom Enterprise
// hosts. Match its case-insensitive URL identity without merging schemes or ports.
export function cloudRepositoryUrlKey(value: string): string {
return repositoryUrlKey(value).toLowerCase();
}

// Cloud ImportModel applies these constraints to both requests and responses.
// Its UTF-8 limits are not exported as JSON Schema character limits: even a
// plain string field such as a receipt error message inherits the 512-byte bound.
Expand Down
2 changes: 1 addition & 1 deletion sdk/typescript/src/external-import-models.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export type Id = string;
export type RepoConnectorId = string;
export type EnvironmentId = string;
export type ResetMarker = string;
export type Provider = "wiz" | "endor" | "snyk" | "github";
export type Provider = "wiz" | "endor" | "snyk" | "github" | "semgrep";
export type SourceKey = string;
export type ClientId = string;
export type SourceFindingId = string;
Expand Down
7 changes: 7 additions & 0 deletions sdk/typescript/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,3 +280,10 @@ export {
normalizeEndorSarif,
readEndorFindings,
} from "./endor-findings.js";

export {
normalizeSemgrepFindings,
normalizeSemgrepSarif,
readSemgrepFindings,
type SemgrepImportOptions,
} from "./semgrep-findings.js";
Loading
Loading