diff --git a/docs/scanner-imports/README.md b/docs/scanner-imports/README.md index ca4f926b6f..1d5a00175f 100644 --- a/docs/scanner-imports/README.md +++ b/docs/scanner-imports/README.md @@ -15,11 +15,12 @@ workbench or send a supported report to Codex Security Cloud. The `--provider` value selects the reader for a saved report. To see which readers your installed CLI supports, run `codex-security publish findings --help`. -The guides cover local and Cloud imports unless an input is marked local only. +The guides cover local and Cloud imports unless a row limits its scope. | Guide | `--provider` | Supported input | | ----------------------------------------------------------- | ------------ | ------------------------------------------------------------------------------- | | [Amazon Inspector](inspector.md) | `inspector` | Inspector2 `ListFindings` JSON for one Code Security repository | +| [ArmorCode](armorcode.md) | `armorcode` | Findings API JSON for one repository (local verified, Cloud unverified) | | [AWS Continuum](continuum.md) | `continuum` | Summaries or selected code finding details for one verified repository | | [Black Duck SCA](blackduck.md) | `blackduck` | BOM-6 vulnerable components for one project version | | [Checkmarx SAST](checkmarx.md) | `checkmarx` | Checkmarx One CLI JSON or self-managed CxSAST XML for one scan | diff --git a/docs/scanner-imports/armorcode.md b/docs/scanner-imports/armorcode.md new file mode 100644 index 0000000000..dae6c5461b --- /dev/null +++ b/docs/scanner-imports/armorcode.md @@ -0,0 +1,53 @@ +# Import ArmorCode findings + +Import a saved response from ArmorCode's `POST /api/findings` endpoint into the +local workbench. Cloud publication of ArmorCode findings is unverified. The reader +accepts the API's `data.findings` envelope, as well as a top-level `findings` +array for exported reports. Each finding needs a stable numeric or string `id` +and a `title`. Reuse the same source key for repeat imports from the same +ArmorCode organization and finding selection. + +Choose SAST findings for one repository in ArmorCode. Set `ARMORCODE_QUERY` to +the path of the JSON request body for that selection. Severity and status +filters alone can return findings from multiple repositories. From the checkout +root, save the response in a private temporary directory outside the checkout. + +```sh +report_dir=$(mktemp -d "${TMPDIR:-/tmp}/armorcode.XXXXXX") +curl --fail-with-body -X POST "https://$HOST_URL/api/findings" \ + -H 'accept: application/json' \ + -H "authorization: Bearer $ARMORCODE_API_KEY" \ + -H 'content-type: application/json' \ + --data-binary "@$ARMORCODE_QUERY" > "$report_dir/armorcode-findings.json" +``` + +Preview the same file against the selected checkout. + +```sh +codex-security publish findings "$report_dir/armorcode-findings.json" \ + --to local --repository . --provider armorcode \ + --source-key ORG_ID/APP_ID --dry-run --format json +``` + +If any SAST row omits its repository URL, verify the report's repository in +ArmorCode and add `--source-repository-url URL` to the command. The import +checks declared repository URLs against the checkout's configured remotes. +A conflicting repository in the report causes the import to fail. + +The import preserves the complete source record as evidence. It maps severity, +description, mitigation, finding URLs, CVE identifiers, file path and line +number, and the last-updated timestamp when present. The evidence URL uses the +source scanner's `url` when present. ArmorCode's `findingUrl` remains in the +retained source record. Findings whose `scanType` contains `SAST` also expose +their source status, scanner origin and verdict, rule IDs, CWE IDs, and +mitigation as typed SAST details. A record's `sourceRepositoryUrl`, +`repositoryUrl`, or repository URL is projected when present. Unknown severities +are retained as `unknown`. The local assessment starts as `not_assessed`. +Repeated IDs in a single input are excluded as ambiguous. Repeat imports with +the same provider, source key and finding ID update the existing observation. + +Review the preview's findings, exclusions, repository and source key. Remove +`--dry-run` to save the findings, then run +`codex-security findings imported . --format json` to read them back. +Records omitted from a later import remain stored. Use the normalized field +names above when an export uses different field names. diff --git a/docs/scanner-imports/schema-guide.md b/docs/scanner-imports/schema-guide.md index 7767d62ac1..33b0f7879d 100644 --- a/docs/scanner-imports/schema-guide.md +++ b/docs/scanner-imports/schema-guide.md @@ -24,6 +24,7 @@ which changes can give an occurrence a different ID. | Report | What identifies one record | | ------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | [Amazon Inspector](inspector.md), [Endor Labs](endor.md), [SonarQube Cloud](sonar.md) | Inspector finding ARN, Endor Finding UUID or Sonar issue key. Endor native JSON and SARIF use the same UUID. | +| [ArmorCode](armorcode.md) | ArmorCode finding ID (`id`) within the source key. | | [AWS Continuum](continuum.md) | Continuum finding ARN. A list summary and a detailed response for that ARN identify the same finding. | | [Tenable One](tenable.md) | Exact Tenable `finding_id` within the local checkout and a stable tenant/asset source key. Use the Code Repository asset UUID in the key. | | [Checkmarx SAST](checkmarx.md) | Checkmarx One uses the similarity ID and result hash. CxSAST scopes the similarity ID and ordered flow to the project and query. | diff --git a/sdk/typescript/schemas/external-findings.schema.json b/sdk/typescript/schemas/external-findings.schema.json index 5fb9905e81..1830317b08 100644 --- a/sdk/typescript/schemas/external-findings.schema.json +++ b/sdk/typescript/schemas/external-findings.schema.json @@ -390,7 +390,8 @@ "sonar", "continuum", "checkmarx", - "trivy" + "trivy", + "armorcode" ], "title": "Provider", "type": "string" diff --git a/sdk/typescript/scripts/package-dist-files.mjs b/sdk/typescript/scripts/package-dist-files.mjs index 8d9fb29d2f..b40fd1354b 100644 --- a/sdk/typescript/scripts/package-dist-files.mjs +++ b/sdk/typescript/scripts/package-dist-files.mjs @@ -34,6 +34,7 @@ const distModules = [ "external-findings-import", "external-findings-read", "scanner-import", + "armorcode-findings", "inspector-findings", "continuum-findings", "checkmarx-evidence", diff --git a/sdk/typescript/src/armorcode-findings.ts b/sdk/typescript/src/armorcode-findings.ts new file mode 100644 index 0000000000..c7c4e89405 --- /dev/null +++ b/sdk/typescript/src/armorcode-findings.ts @@ -0,0 +1,192 @@ +import type { ExternalImportTarget } from "./external-import-contract.js"; +import { validateExternalEvidence } from "./external-import-contract.js"; +import type { ExternalFindings } from "./external-findings.js"; +import type { ExternalFindingEvidence } from "./external-import-models.js"; +import { declaredScannerRepositoryUrl } from "./scanner-repository-url.js"; +import { isRecord } from "./record.js"; +import { + normalizeRecords, + record, + requiredText, + text, +} from "./scanner-import.js"; + +type ArmorCodeFinding = Record; + +function armorCodeId(value: unknown): string { + if (typeof value === "string") + return requiredText(value, "ArmorCode finding ID"); + if (typeof value === "number" && Number.isSafeInteger(value)) + return String(value); + throw new Error("ArmorCode finding ID must be text or a safe integer."); +} + +function stringValues(value: unknown): string[] { + const values = Array.isArray(value) ? value : value == null ? [] : [value]; + return values.flatMap((item) => { + if (typeof item === "string" && item.trim()) return [item]; + if (typeof item === "number" && Number.isSafeInteger(item)) + return [String(item)]; + return []; + }); +} + +/** Recover the source repository independently of the finding's scan type. */ +export function armorCodeRepositoryUrl( + finding: ArmorCodeFinding | null | undefined, + target: ExternalImportTarget, +): string | undefined { + const repository = finding?.["repository"]; + const url = + text(finding?.["sourceRepositoryUrl"]) ?? + text(finding?.["repositoryUrl"]) ?? + text(isRecord(repository) ? repository["url"] : repository); + return url + ? declaredScannerRepositoryUrl(url, "ArmorCode", target) + : undefined; +} + +/** Normalize stable finding records while retaining the original ArmorCode row. */ +export function normalizeArmorCodeFindings( + input: unknown, + target: ExternalImportTarget = "cloud", + sourceRepositoryUrl?: string, +): ExternalFindings { + const document = Array.isArray(input) + ? { findings: input } + : record(input, "ArmorCode report"); + const raw = + document["findings"] ?? + document["issues"] ?? + (isRecord(document["data"]) ? document["data"]["findings"] : undefined); + if (!Array.isArray(raw)) + throw new Error("ArmorCode report must contain a findings array."); + const report = { ...document }; + delete report["findings"]; + delete report["issues"]; + if (isRecord(report["data"])) { + const data = { ...report["data"] }; + delete data["findings"]; + if (Object.keys(data).length) report["data"] = data; + else delete report["data"]; + } + const declaredUrl = + sourceRepositoryUrl === undefined + ? undefined + : declaredScannerRepositoryUrl(sourceRepositoryUrl, "ArmorCode", target); + return normalizeRecords( + raw as ArmorCodeFinding[], + (finding) => { + const severity = (text(finding["severity"]) ?? "unknown").toLowerCase(); + const locations = Array.isArray(finding["locations"]) + ? finding["locations"] + : finding["location"] + ? [finding["location"]] + : text(finding["filePath"]) + ? [{ path: finding["filePath"], line: finding["lineNumber"] }] + : []; + const description = text( + finding["description"] ?? + finding["details"] ?? + finding["toolDescription"], + ); + const mitigation = text(finding["mitigation"]); + const lastUpdated = finding["lastUpdated"]; + const isSast = + Array.isArray(finding["scanType"]) && + finding["scanType"].some( + (value) => + typeof value === "string" && value.toLowerCase() === "sast", + ); + const ruleIds = stringValues(finding["ruleId"]); + const weaknessIds = [ + ...stringValues(finding["cwe"]), + ...stringValues(finding["cwesStrings"]), + ]; + const repositoryUrl = + armorCodeRepositoryUrl(finding, target) ?? declaredUrl; + if (isSast && !repositoryUrl) + throw new Error( + "ArmorCode SAST evidence requires its source repository. For a single-repository report, supply its verified --source-repository-url.", + ); + const evidence: ExternalFindingEvidence = { + title: requiredText( + finding["title"] ?? finding["name"] ?? finding["findingName"], + "ArmorCode finding title", + ), + severity: ([ + "critical", + "high", + "medium", + "low", + "informational", + ].includes(severity) + ? severity + : "unknown") as ExternalFindingEvidence["severity"], + description: isSast + ? description + : description && mitigation + ? `${description}\n\nMitigation: ${mitigation}` + : (description ?? mitigation), + // ArmorCode's findingUrl remains available in source_data; url points + // to the originating scanner when ArmorCode provides both links. + url: text(finding["url"] ?? finding["findingUrl"]), + ...(Array.isArray(finding["cve"]) && { + advisory_ids: finding["cve"].filter( + (value): value is string => typeof value === "string", + ), + }), + locations: locations.flatMap((value) => { + if (!isRecord(value)) return []; + const path = text( + value["path"] ?? value["filePath"] ?? value["file"], + ); + if (!path) return []; + const line = + value["line"] ?? value["startLine"] ?? value["lineNumber"]; + return [ + { + path, + line: + Number.isSafeInteger(line) && Number(line) > 0 + ? Number(line) + : null, + }, + ]; + }), + ...(typeof lastUpdated === "number" && + Number.isSafeInteger(lastUpdated) && { + source_updated_at: Math.floor(lastUpdated / 1000), + }), + ...(isSast && { + details: { + kind: "sast", + ...(repositoryUrl && { repository: { url: repositoryUrl } }), + source_status: text( + finding["status"] ?? finding["toolFindingStatus"], + ), + scanner_origin: text(finding["source"]), + scanner_verdict: text(finding["toolStatusOriginal"]), + ...(ruleIds.length > 0 && { rule: { id: ruleIds.join(", ") } }), + ...(weaknessIds.length > 0 && { + weakness_ids: [...new Set(weaknessIds)], + }), + ...(mitigation && { remediation_instructions: mitigation }), + }, + }), + source_data: structuredClone(finding), + }; + validateExternalEvidence(evidence, target); + return evidence; + }, + (finding) => + armorCodeId( + finding["source_finding_id"] ?? + finding["findingId"] ?? + finding["id"] ?? + finding["finding_id"], + ), + target, + { report }, + ); +} diff --git a/sdk/typescript/src/external-findings-local.ts b/sdk/typescript/src/external-findings-local.ts index e99f977ed3..4d031edd90 100644 --- a/sdk/typescript/src/external-findings-local.ts +++ b/sdk/typescript/src/external-findings-local.ts @@ -77,6 +77,7 @@ import { continuumReportRepositories } from "./continuum-findings.js"; import { veracodeReportRepositories } from "./veracode-findings.js"; import { inspectorReportRepository } from "./inspector-findings.js"; import { declaredScannerRepositoryUrl } from "./scanner-repository-url.js"; +import { armorCodeRepositoryUrl } from "./armorcode-findings.js"; import { sonarRepositoryUrls } from "./sonar-findings.js"; import { isRecord } from "./record.js"; import { dependabotRepository } from "./github-dependabot-findings.js"; @@ -994,6 +995,10 @@ export async function prepareLocalImport( if (semgrep.migration) Object.assign(finding.evidence, semgrep.migration); } const { evidence } = finding; + if (options.provider === "armorcode") { + const nativeUrl = armorCodeRepositoryUrl(evidence.source_data, "local"); + if (nativeUrl) sources.set(nativeUrl, { url: nativeUrl }); + } if (options.provider === "github") { const source = retainedGitHubRepository( finding, @@ -1242,6 +1247,20 @@ export async function prepareLocalImport( throw new Error( "The retained Checkmarx report repository does not match the finding repository.", ); + if (options.provider === "armorcode") { + if (options.sourceRepositoryUrl !== undefined) { + const url = declaredScannerRepositoryUrl( + options.sourceRepositoryUrl, + "ArmorCode", + "local", + ); + sources.set(url, { url }); + } + if (!sameRepositories(sources.keys())) + throw new CodexSecurityError( + "ArmorCode findings must belong to a single source repository.", + ); + } const sonarSources = sonarRepositoryUrls( options.provider, current.findings, diff --git a/sdk/typescript/src/external-findings-publish.ts b/sdk/typescript/src/external-findings-publish.ts index 60ef3fc40a..e49cb36cdb 100644 --- a/sdk/typescript/src/external-findings-publish.ts +++ b/sdk/typescript/src/external-findings-publish.ts @@ -19,6 +19,7 @@ import { } from "./private-finding-import.js"; import { readCloudCredentials } from "./cloud-publish.js"; import type { LocalExternalImportOptions } from "./external-findings-import.js"; +import { armorCodeRepositoryUrl } from "./armorcode-findings.js"; import { declaredScannerRepositoryUrl } from "./scanner-repository-url.js"; import { tenableReportFormat } from "./tenable-findings.js"; import { @@ -1418,6 +1419,8 @@ async function preparePublicationInput( `Finding ${JSON.stringify(id)} belongs to ${JSON.stringify(url)}, which does not match the selected Cloud repository ${JSON.stringify(destination.url)}. Verify the source repository mapping before publishing.`, ); } + if (provider === "armorcode") + requireDestination("ArmorCode source", declaredUrl); if (report?.["format"] === guardianSelectionFormat) requireDestination( "Guardian source", @@ -1478,6 +1481,7 @@ async function preparePublicationInput( if (binding) requireDestination(binding.findingId, binding.url); } const checkOnlyProjected = !checkNative( + "armorcode", "blackduck", "inspector", "veracode", @@ -1511,7 +1515,9 @@ async function preparePublicationInput( checkmarxRepository(finding)?.url, evidence.details?.repository?.url, ]; - if (provider === "wiz") { + if (provider === "armorcode") { + urls.push(armorCodeRepositoryUrl(evidence.source_data, "cloud")); + } else if (provider === "wiz") { const native = hasProjection || (!historical && isWizPackageFinding(evidence.source_data)); diff --git a/sdk/typescript/src/external-findings-read.ts b/sdk/typescript/src/external-findings-read.ts index 5fcb5b316d..1bb3f20416 100644 --- a/sdk/typescript/src/external-findings-read.ts +++ b/sdk/typescript/src/external-findings-read.ts @@ -35,12 +35,14 @@ import { readCheckmarxFindings } from "./checkmarx-findings.js"; import { normalizeInspectorFindings } from "./inspector-findings.js"; import { isRecord } from "./record.js"; import { readWizSelection, wizFindingRepository } from "./wiz-findings.js"; +import { normalizeArmorCodeFindings } from "./armorcode-findings.js"; export type LocalFindingProvider = Provider | "tenable"; /** File reader names mapped to the provider stored with each finding. */ export const EXTERNAL_FINDINGS_PROVIDERS = { wiz: "wiz", + armorcode: "armorcode", snyk: "snyk", semgrep: "semgrep", trivy: "trivy", @@ -221,6 +223,16 @@ export async function readExternalFindings( sourceRepositories.push({ url: options.sourceRepositoryUrl }); return { findings, sourceRepositories }; } + if (provider === "armorcode") { + const input = await readScannerJson(path); + return { + findings: normalizeArmorCodeFindings( + input, + target, + options.sourceRepositoryUrl, + ), + }; + } if (provider === "github") return { findings: await readGitHubFindings(path, target) }; if (provider === "wiz") { diff --git a/sdk/typescript/src/external-import-models.ts b/sdk/typescript/src/external-import-models.ts index d19314b29a..26ef1ff684 100644 --- a/sdk/typescript/src/external-import-models.ts +++ b/sdk/typescript/src/external-import-models.ts @@ -17,7 +17,8 @@ export type Provider = | "sonar" | "continuum" | "checkmarx" - | "trivy"; + | "trivy" + | "armorcode"; export type SourceKey = string; export type ClientId = string; export type SourceFindingId = string; diff --git a/sdk/typescript/tests-ts/armorcode-findings.test.ts b/sdk/typescript/tests-ts/armorcode-findings.test.ts new file mode 100644 index 0000000000..e0303f617c --- /dev/null +++ b/sdk/typescript/tests-ts/armorcode-findings.test.ts @@ -0,0 +1,182 @@ +import { describe, expect, test } from "bun:test"; +import { normalizeArmorCodeFindings } from "../src/armorcode-findings.js"; +import { localReportFixture } from "./external-findings-local-helpers.js"; + +describe("ArmorCode finding imports", () => { + const repositoryUrl = "https://github.com/example/project"; + const source = { provider: "armorcode", sourceKey: "org/app" } as const; + + test("uses a declared source for SAST rows without changing the stored source record", async () => { + await using fixture = await localReportFixture("armorcode.json", source, { + remote: repositoryUrl, + }); + const row = { id: 1, title: "Unchecked index", scanType: ["SAST"] }; + await fixture.writeJson({ data: { findings: [row] } }); + const { preview } = await fixture.prepare({ + sourceRepositoryUrl: repositoryUrl, + }); + expect(preview.excluded).toEqual([]); + expect(preview.findings[0]?.evidence.details).toMatchObject({ + kind: "sast", + repository: { url: repositoryUrl }, + }); + expect(preview.findings[0]?.evidence.source_data).toEqual(row); + await expect( + fixture.prepare({ + sourceRepositoryUrl: "https://github.com/example/other", + }), + ).rejects.toThrow(/does not match/iu); + }); + + test("rejects a mixed report when a generic row belongs to another checkout", async () => { + await using fixture = await localReportFixture("armorcode.json", source, { + remote: repositoryUrl, + }); + const report = { + data: { + findings: [ + { + id: 1, + title: "Unchecked index", + scanType: ["SAST"], + repositoryUrl, + }, + { + id: 2, + title: "Vulnerable package", + scanType: ["SCA"], + repository: { url: "https://github.com/example/other" }, + }, + ], + }, + }; + await fixture.writeJson(report); + await expect(fixture.prepare()).rejects.toThrow( + /single source repository/iu, + ); + await expect( + fixture.prepareNormalized(normalizeArmorCodeFindings(report, "local")), + ).rejects.toThrow(/single source repository/iu); + }); + + test("normalizes findings and retains the source records", () => { + const result = normalizeArmorCodeFindings({ + findings: [ + { + findingId: "AC-42", + name: "Unsafe query construction", + severity: "HIGH", + description: "Untrusted input reaches a query.", + location: { filePath: "src/query.ts", startLine: 18 }, + status: "open", + }, + ], + }); + + expect(result.findings).toHaveLength(1); + expect(result.findings[0]).toMatchObject({ + source_finding_id: "AC-42", + evidence: { + title: "Unsafe query construction", + severity: "high", + locations: [{ path: "src/query.ts", line: 18 }], + source_data: { status: "open" }, + }, + }); + }); + + test("excludes colliding source IDs instead of selecting one", () => { + const result = normalizeArmorCodeFindings( + [ + { id: "same", title: "First" }, + { id: "same", title: "Second" }, + ], + "local", + ); + + expect(result.findings).toHaveLength(0); + expect(result.excluded).toHaveLength(2); + }); + + test("normalizes the ArmorCode findings API response envelope", () => { + const result = normalizeArmorCodeFindings({ + data: { + findings: [ + { + id: 1097722461, + title: "Container(s) run as root", + severity: "CRITICAL", + description: "Container is configured to run as root.", + mitigation: "Set runAsUser to a non-zero ID.", + findingUrl: "https://armorcode.example/findings/1097722461", + filePath: "deploy/workload.yaml", + lineNumber: 12, + cve: [], + status: "OPEN", + lastUpdated: 1792740664000, + }, + ], + }, + success: true, + }); + + expect(result.findings).toHaveLength(1); + expect(result.findings[0]).toMatchObject({ + source_finding_id: "1097722461", + evidence: { + title: "Container(s) run as root", + severity: "critical", + description: + "Container is configured to run as root.\n\nMitigation: Set runAsUser to a non-zero ID.", + url: "https://armorcode.example/findings/1097722461", + locations: [{ path: "deploy/workload.yaml", line: 12 }], + source_updated_at: 1792740664, + source_data: { status: "OPEN" }, + }, + }); + }); + + test("projects typed SAST metadata and keeps both scanner links", () => { + const result = normalizeArmorCodeFindings({ + data: { + findings: [ + { + id: 42, + title: "Buffer index access", + repositoryUrl: "https://github.com/example/repository", + severity: "HIGH", + description: "Query: Buffer index access", + mitigation: "Validate the buffer index.", + source: "Checkmarx", + status: "OPEN", + toolStatusOriginal: "TO VERIFY", + scanType: ["SAST"], + ruleId: ["CX-101"], + cwe: "CWE-129", + url: "https://scanner.example/findings/42", + findingUrl: "https://armorcode.example/findings/42", + }, + ], + }, + }); + + expect(result.findings[0]).toMatchObject({ + source_finding_id: "42", + evidence: { + url: "https://scanner.example/findings/42", + description: "Query: Buffer index access", + details: { + kind: "sast", + repository: { url: "https://github.com/example/repository" }, + source_status: "OPEN", + scanner_origin: "Checkmarx", + scanner_verdict: "TO VERIFY", + rule: { id: "CX-101" }, + weakness_ids: ["CWE-129"], + remediation_instructions: "Validate the buffer index.", + }, + source_data: { findingUrl: "https://armorcode.example/findings/42" }, + }, + }); + }); +}); diff --git a/sdk/typescript/tests-ts/external-findings-publish.test.ts b/sdk/typescript/tests-ts/external-findings-publish.test.ts index a16d2dfb7a..85040484f2 100644 --- a/sdk/typescript/tests-ts/external-findings-publish.test.ts +++ b/sdk/typescript/tests-ts/external-findings-publish.test.ts @@ -21,6 +21,7 @@ import { import { endorFinding, endorSarif } from "./support/endor-import-fixtures.js"; import { sonarGitTestEnvironment } from "./support/sonar-import-fixtures.js"; import { normalizeEndorFindings } from "../src/endor-findings.js"; +import { normalizeArmorCodeFindings } from "../src/armorcode-findings.js"; import { createServer, type ServerResponse } from "node:http"; import { execFile } from "node:child_process"; import { hash } from "node:crypto"; @@ -308,6 +309,45 @@ test("Wiz's gzip JSON report retains finding identity, evidence and repository l ).toBeNull(); }); +test("ArmorCode rejects generic rows from a different Cloud repository before publishing", async () => { + const sourceRepositoryUrl = "https://github.com/example/project"; + const report = { + data: { + findings: [ + { + id: 1, + title: "Unchecked index", + scanType: ["SAST"], + repositoryUrl: sourceRepositoryUrl, + }, + { + id: 2, + title: "Vulnerable package", + scanType: ["SCA"], + repositoryUrl: "https://github.com/example/other", + }, + ], + }, + }; + const f = await fixture(report, sourceRepositoryUrl); + const selection = { + ...options, + provider: "armorcode" as const, + sourceRepositoryUrl, + }; + await expect( + prepareExternalPublication(f.file, selection, f.deps), + ).rejects.toThrow("does not match the selected Cloud repository"); + await expect( + prepareNormalizedExternalPublication( + normalizeArmorCodeFindings(report), + selection, + f.deps, + ), + ).rejects.toThrow("does not match the selected Cloud repository"); + expect(f.posts).toHaveLength(0); +}); + test("a Wiz Raw Event export explains the finding-report path before contacting Cloud", async () => { const f = await fixture({ id: "scan-event-1",