diff --git a/.github/workflows/endor-scanner-image.yml b/.github/workflows/endor-scanner-image.yml new file mode 100644 index 0000000000..29c6d7d4c6 --- /dev/null +++ b/.github/workflows/endor-scanner-image.yml @@ -0,0 +1,42 @@ +name: Endor Scanner Image + +on: + workflow_dispatch: + +permissions: + contents: read + packages: write + +jobs: + publish: + if: github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + timeout-minutes: 15 + env: + IMAGE: ghcr.io/${{ github.repository_owner }}/clawhub-endor:${{ github.sha }} + ENDORCTL_VERSION: v1.7.1115 + ENDORCTL_SHA256: 4022e91a39d04f852f8a6195a5f98297af6cf2738f4012b1591bc87976136d4b + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Build and check scanner image + run: | + set -euo pipefail + curl --fail --location --retry 3 \ + "https://api.endorlabs.com/download/endorlabs/$ENDORCTL_VERSION/binaries/endorctl_${ENDORCTL_VERSION}_linux_amd64" \ + --output scripts/security/endor/endorctl + docker build --platform linux/amd64 \ + --build-arg "ENDORCTL_SHA256=$ENDORCTL_SHA256" \ + --label "org.opencontainers.image.source=https://github.com/$GITHUB_REPOSITORY" \ + --tag "$IMAGE" scripts/security/endor + docker run --rm --network none "$IMAGE" test -x /usr/local/bin/clawhub-endor-scan + + - name: Publish image and report digest + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + printf '%s' "$GH_TOKEN" | docker login ghcr.io --username "$GITHUB_ACTOR" --password-stdin + docker push "$IMAGE" + digest=$(docker image inspect --format '{{index .RepoDigests 0}}' "$IMAGE") + printf 'Set CODEX_SECURITY_SCAN_ENDOR_IMAGE to %s before enabling Endor.\n' "$digest" >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/security-scan-codex.yml b/.github/workflows/security-scan-codex.yml index 7df783738b..b7fa6673f1 100644 --- a/.github/workflows/security-scan-codex.yml +++ b/.github/workflows/security-scan-codex.yml @@ -32,6 +32,7 @@ on: default: "12" permissions: contents: read + packages: read jobs: codex-security-scan: @@ -62,6 +63,9 @@ jobs: CODEX_SECURITY_SCAN_ASSIGNED_JOBS: ${{ inputs['assigned-jobs'] || '' }} CODEX_SECURITY_SCAN_LANE: ${{ matrix.lane }} CODEX_SECURITY_SCAN_CLAWSCAN_TIMEOUT_MS: ${{ vars.CODEX_SECURITY_SCAN_CLAWSCAN_TIMEOUT_MS || '900000' }} + CODEX_SECURITY_SCAN_CLAWSCAN_VERSION: ${{ vars.CODEX_SECURITY_SCAN_CLAWSCAN_VERSION || '0.2.0' }} + CODEX_SECURITY_SCAN_ENDOR_ENABLED: ${{ vars.CODEX_SECURITY_SCAN_ENDOR_ENABLED || '0' }} + CODEX_SECURITY_SCAN_ENDOR_IMAGE: ${{ vars.CODEX_SECURITY_SCAN_ENDOR_IMAGE }} # The runner is disposable and installs the reviewed, hash-locked host scanners above. CODEX_SECURITY_SCAN_CLAWSCAN_SANDBOX: "off" CODEX_SECURITY_SCAN_LEASE_MINUTES: "60" @@ -89,9 +93,24 @@ jobs: - name: Install ClawScan CLI run: | set -euo pipefail - npm install -g @openclaw/clawscan@0.1.8 + [[ "$CODEX_SECURITY_SCAN_CLAWSCAN_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo 'ClawScan requires an exact release version'; exit 1; } + npm install -g "@openclaw/clawscan@$CODEX_SECURITY_SCAN_CLAWSCAN_VERSION" clawscan --version + - name: Prepare Endor scanner + if: ${{ env.CODEX_SECURITY_SCAN_ENDOR_ENABLED == '1' }} + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + [[ "$CODEX_SECURITY_SCAN_ENDOR_IMAGE" =~ @sha256:[a-f0-9]{64}$ ]] || { echo 'Endor requires a Docker image pinned by SHA-256 digest'; exit 1; } + export DOCKER_CONFIG + DOCKER_CONFIG=$(mktemp -d) + trap 'rm -rf "$DOCKER_CONFIG"' EXIT + printf '%s' "$GH_TOKEN" | docker login ghcr.io --username "$GITHUB_ACTOR" --password-stdin + docker pull "$CODEX_SECURITY_SCAN_ENDOR_IMAGE" + docker run --rm --network none "$CODEX_SECURITY_SCAN_ENDOR_IMAGE" test -x /usr/local/bin/clawhub-endor-scan + - name: Install A.I.G scanner run: | set -euo pipefail @@ -123,6 +142,10 @@ jobs: LLM_API_KEY: ${{ secrets.OPENAI_API_KEY || secrets.CODEX_API_KEY }} OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} SECURITY_SCAN_WORKER_TOKEN: ${{ secrets.SECURITY_SCAN_WORKER_TOKEN }} + ENDOR_NAMESPACE: ${{ vars.ENDOR_NAMESPACE }} + ENDOR_API: ${{ vars.ENDOR_API }} + ENDOR_API_CREDENTIALS_KEY: ${{ secrets.ENDOR_API_CREDENTIALS_KEY }} + ENDOR_API_CREDENTIALS_SECRET: ${{ secrets.ENDOR_API_CREDENTIALS_SECRET }} run: | bun scripts/security/run-codex-scan-worker.ts \ --batch-limit "$CODEX_SECURITY_SCAN_LIMIT" \ diff --git a/convex/_generated/api.d.ts b/convex/_generated/api.d.ts index e27acf0043..34a8c0fbfd 100644 --- a/convex/_generated/api.d.ts +++ b/convex/_generated/api.d.ts @@ -89,6 +89,7 @@ import type * as lib_emailRendering from "../lib/emailRendering.js"; import type * as lib_emails from "../lib/emails.js"; import type * as lib_embeddingVisibility from "../lib/embeddingVisibility.js"; import type * as lib_embeddings from "../lib/embeddings.js"; +import type * as lib_endorAnalysis from "../lib/endorAnalysis.js"; import type * as lib_experimentalClaws from "../lib/experimentalClaws.js"; import type * as lib_featuredIntelligence from "../lib/featuredIntelligence.js"; import type * as lib_featuredPolicy from "../lib/featuredPolicy.js"; @@ -160,6 +161,7 @@ import type * as lib_searchText from "../lib/searchText.js"; import type * as lib_securityPrompt from "../lib/securityPrompt.js"; import type * as lib_securityScanPolicy from "../lib/securityScanPolicy.js"; import type * as lib_skillBackfill from "../lib/skillBackfill.js"; +import type * as lib_skillCardClaims from "../lib/skillCardClaims.js"; import type * as lib_skillCards from "../lib/skillCards.js"; import type * as lib_skillDownloadBackfill from "../lib/skillDownloadBackfill.js"; import type * as lib_skillEvaluationConfig from "../lib/skillEvaluationConfig.js"; @@ -347,6 +349,7 @@ declare const fullApi: ApiFromModules<{ "lib/emails": typeof lib_emails; "lib/embeddingVisibility": typeof lib_embeddingVisibility; "lib/embeddings": typeof lib_embeddings; + "lib/endorAnalysis": typeof lib_endorAnalysis; "lib/experimentalClaws": typeof lib_experimentalClaws; "lib/featuredIntelligence": typeof lib_featuredIntelligence; "lib/featuredPolicy": typeof lib_featuredPolicy; @@ -418,6 +421,7 @@ declare const fullApi: ApiFromModules<{ "lib/securityPrompt": typeof lib_securityPrompt; "lib/securityScanPolicy": typeof lib_securityScanPolicy; "lib/skillBackfill": typeof lib_skillBackfill; + "lib/skillCardClaims": typeof lib_skillCardClaims; "lib/skillCards": typeof lib_skillCards; "lib/skillDownloadBackfill": typeof lib_skillDownloadBackfill; "lib/skillEvaluationConfig": typeof lib_skillEvaluationConfig; diff --git a/convex/devSeed.ts b/convex/devSeed.ts index 67b21fa030..453a7fd88c 100644 --- a/convex/devSeed.ts +++ b/convex/devSeed.ts @@ -3733,6 +3733,18 @@ export async function seedLocalModerationFixturesHandler( }, llmAnalysis: pluginClawScanRiskAnalysis(now), staticScan: scannedStaticScan, + endorAnalysis: { + status: "completed", + checkedAt: now, + reachableFunctionCount: 1, + findings: [ + { + severity: "high", + summary: + "Synthetic reachability fixture: a vulnerable function in fixture-parser@1.0.0 is reachable from the plugin entrypoint.", + }, + ], + }, source: { kind: "github", repo: "openclaw/local-dev-fixture", path: "." }, createdBy: userId, publishActor: { kind: "user", userId }, diff --git a/convex/httpApiV1.handlers.test.ts b/convex/httpApiV1.handlers.test.ts index 0d508d8b44..03cbcd9f40 100644 --- a/convex/httpApiV1.handlers.test.ts +++ b/convex/httpApiV1.handlers.test.ts @@ -540,6 +540,63 @@ describe("httpApiV1 handlers", () => { expect(readme).toContain("`clawscan.json`: final ClawScan verdict"); }); + it("includes the bounded Endor summary in stored plugin report downloads", async () => { + vi.mocked(requireApiTokenUser).mockResolvedValue({ + userId: "users:owner", + user: { _id: "users:owner", role: "user" }, + } as never); + const endor = { + status: "completed", + checkedAt: 2, + reachableFunctionCount: 1, + findings: [{ severity: "high", summary: "Reachable vulnerable function" }], + }; + const runQuery = vi.fn(async (_query: unknown, args: Record) => { + if (isRateLimitArgs(args)) return okRate(); + expect(args).toMatchObject({ + actorUserId: "users:owner", + kind: "plugin", + name: "demo-plugin", + version: "1.2.3", + }); + return { + ok: true, + scanId: "plugin:demo-plugin:1.2.3", + status: "succeeded", + sourceKind: "published", + update: false, + writtenBack: true, + artifact: { kind: "plugin", name: "demo-plugin", version: "1.2.3" }, + report: { + clawscan: { status: "clean", checkedAt: 1 }, + endor, + skillspector: null, + staticAnalysis: null, + virustotal: null, + }, + createdAt: 1, + updatedAt: 2, + completedAt: 2, + }; + }); + const response = await __handlers.skillScanGetRouterV1Handler( + makeCtx({ runQuery }), + new Request( + "https://example.com/api/v1/skills/-/scan/download/demo-plugin?version=1.2.3&kind=plugin", + ), + ); + + expect(response.status).toBe(200); + const entries = unzipSync(new Uint8Array(await response.arrayBuffer())); + const endorEntry = entries["endor.json"]; + const readmeEntry = entries["README.md"]; + if (!endorEntry || !readmeEntry) throw new Error("Expected scan report entries missing"); + expect(JSON.parse(strFromU8(endorEntry))).toEqual(endor); + expect(strFromU8(readmeEntry)).toContain( + "a missing or skipped Endor result is not a clean result", + ); + }); + it("forwards the owner namespace when downloading a stored skill report", async () => { vi.mocked(requireApiTokenUser).mockResolvedValue({ userId: "users:owner", @@ -14459,6 +14516,12 @@ describe("httpApiV1 handlers", () => { summary: "Looks safe.", checkedAt: 1, }, + endorAnalysis: { + status: "completed", + checkedAt: 2, + reachableFunctionCount: 2, + findings: [{ severity: "high", summary: "Reachable dependency finding" }], + }, staticScan: { status: "malicious", reasonCodes: ["malicious.static_fixture"], @@ -14496,6 +14559,11 @@ describe("httpApiV1 handlers", () => { status: "clean", verdict: "clean", }, + endorAnalysis: { + status: "completed", + reachableFunctionCount: 2, + findings: [{ severity: "high", summary: "Reachable dependency finding" }], + }, verification: { scanStatus: "clean", }, @@ -14913,6 +14981,11 @@ describe("httpApiV1 handlers", () => { npmTarballName: "demo-plugin-1.0.0.tgz", verification: { scanStatus: "malicious" }, manualModeration: { state: "quarantined", reason: "private reviewer note" }, + endorAnalysis: { + status: "skipped", + checkedAt: 2, + reason: "No supported call graph", + }, }, }; } @@ -14953,6 +15026,11 @@ describe("httpApiV1 handlers", () => { npmShasum: "d".repeat(40), npmTarballName: "demo-plugin-1.0.0.tgz", createdAt: 1, + endorAnalysis: { + status: "skipped", + checkedAt: 2, + reason: "No supported call graph", + }, }, trust: { scanStatus: "malicious", diff --git a/convex/httpApiV1/packagesV1.ts b/convex/httpApiV1/packagesV1.ts index aaf92fe111..d99d95f609 100644 --- a/convex/httpApiV1/packagesV1.ts +++ b/convex/httpApiV1/packagesV1.ts @@ -563,6 +563,7 @@ type ReleaseLike = { sha256hash?: string; vtAnalysis?: Doc<"packageReleases">["vtAnalysis"]; skillSpectorAnalysis?: Doc<"packageReleases">["skillSpectorAnalysis"]; + endorAnalysis?: Doc<"packageReleases">["endorAnalysis"]; llmAnalysis?: Doc<"packageReleases">["llmAnalysis"]; staticScan?: Doc<"packageReleases">["staticScan"]; manualModeration?: Doc<"packageReleases">["manualModeration"]; @@ -716,6 +717,7 @@ function toPackageVersionResponse(release: ReleaseLike, packageName: string) { sha256hash: release.sha256hash ?? null, vtAnalysis: release.vtAnalysis ?? null, skillSpectorAnalysis: release.skillSpectorAnalysis ?? null, + ...(release.endorAnalysis ? { endorAnalysis: release.endorAnalysis } : {}), llmAnalysis: release.llmAnalysis ?? null, staticScan: release.staticScan ?? null, }; @@ -753,6 +755,7 @@ function toPackageReleaseSecurityResponse(params: { ...(params.release.npmShasum ? { npmShasum: params.release.npmShasum } : {}), ...(params.release.npmTarballName ? { npmTarballName: params.release.npmTarballName } : {}), createdAt: params.release.createdAt, + ...(params.release.endorAnalysis ? { endorAnalysis: params.release.endorAnalysis } : {}), }, trust: { scanStatus, diff --git a/convex/httpApiV1/skillsV1.ts b/convex/httpApiV1/skillsV1.ts index d62d74dd97..2bf8bebae9 100644 --- a/convex/httpApiV1/skillsV1.ts +++ b/convex/httpApiV1/skillsV1.ts @@ -482,11 +482,13 @@ function buildSkillScanReportZip(status: Record) { "- `clean` means ClawHub did not find blocking security issues in this scan.", "", "VirusTotal results are supporting reputation telemetry. They can help explain a risk signal, but they are not the sole source of ClawHub's final verdict.", + "Endor results summarize reachable dependency findings. They supplement the ClawScan verdict; a missing or skipped Endor result is not a clean result.", "", "## Files", "", "- `manifest.json`: artifact identity, scan status, timestamps, and writeback state.", "- `clawscan.json`: final ClawScan verdict, summary, guidance, and findings.", + "- `endor.json`: bounded Endor reachability summary when available.", "- `skillspector.json`: SkillSpector structure and agentic-risk signals when available.", "- `static-analysis.json`: deterministic scanner findings, reason codes, and static summary.", "- `virustotal.json`: external reputation counts and status when available.", @@ -497,6 +499,7 @@ function buildSkillScanReportZip(status: Record) { return buildDeterministicZip([ { path: "manifest.json", bytes: encodeJsonEntry(manifest) }, { path: "clawscan.json", bytes: encodeJsonEntry(scanReportPart(status, "clawscan")) }, + { path: "endor.json", bytes: encodeJsonEntry(scanReportPart(status, "endor")) }, { path: "skillspector.json", bytes: encodeJsonEntry(scanReportPart(status, "skillspector")) }, { path: "static-analysis.json", diff --git a/convex/lib/endorAnalysis.ts b/convex/lib/endorAnalysis.ts new file mode 100644 index 0000000000..3e5457e699 --- /dev/null +++ b/convex/lib/endorAnalysis.ts @@ -0,0 +1,21 @@ +import { zodToConvex } from "convex-helpers/server/zod4"; +import { z } from "zod"; + +export const endorAnalysisSchema = z.discriminatedUnion("status", [ + z.object({ + status: z.literal("completed"), + checkedAt: z.number().finite(), + reachableFunctionCount: z.number().int().nonnegative(), + findings: z + .array(z.object({ severity: z.string().max(64), summary: z.string().max(2000) })) + .max(50), + }), + z.object({ + status: z.enum(["skipped", "failed"]), + checkedAt: z.number().finite(), + reason: z.string().max(2000), + }), +]); + +export const endorAnalysisValidator = zodToConvex(endorAnalysisSchema); +export type EndorAnalysis = z.infer; diff --git a/convex/packageSecurityScan.runtime.test.ts b/convex/packageSecurityScan.runtime.test.ts new file mode 100644 index 0000000000..c2e71257b1 --- /dev/null +++ b/convex/packageSecurityScan.runtime.test.ts @@ -0,0 +1,264 @@ +/// +/* @vitest-environment edge-runtime */ +import { register as registerRateLimiter } from "@convex-dev/rate-limiter/test"; +import { convexTest } from "convex-test"; +import { expect, it, vi } from "vitest"; +import { internal } from "./_generated/api"; +import type { Doc, Id } from "./_generated/dataModel"; +import schema from "./schema"; + +// Route behavior assumes verified ingress; trust validation is covered by httpRateLimit.edge.test.ts. +vi.mock("./lib/verifiedClientIp", () => ({ + getVerifiedClientIp: async () => "203.0.113.1", +})); + +const modules = import.meta.glob("./**/*.ts"); + +async function createPackageScanFixture() { + const t = convexTest(schema, modules); + registerRateLimiter(t); + const fixture = await t.run(async (ctx) => { + const userId = await ctx.db.insert("users", { handle: "endor-owner" }); + const publisherId = await ctx.db.insert("publishers", { + kind: "user", + handle: "endor-owner", + displayName: "Endor owner", + linkedUserId: userId, + createdAt: 1, + updatedAt: 1, + }); + const verification = { + tier: "structural", + scope: "artifact-only", + scanStatus: "pending", + } as const; + const packageId = await ctx.db.insert("packages", { + name: "endor-plugin", + normalizedName: "endor-plugin", + displayName: "Endor plugin", + ownerUserId: userId, + ownerPublisherId: publisherId, + family: "code-plugin", + channel: "community", + isOfficial: false, + tags: {}, + compatibility: {}, + verification, + scanStatus: "pending", + stats: { downloads: 0, installs: 0, stars: 0, versions: 1 }, + createdAt: 1, + updatedAt: 1, + }); + const releaseId = await ctx.db.insert("packageReleases", { + packageId, + version: "1.0.0", + changelog: "Initial release", + distTags: ["latest"], + files: [], + integritySha256: "a".repeat(64), + compatibility: {}, + verification, + createdBy: userId, + publishActor: { kind: "user", userId }, + createdAt: 1, + }); + const latestVersionSummary: NonNullable["latestVersionSummary"]> = { + version: "1.0.0", + createdAt: 1, + changelog: "Initial release", + compatibility: {}, + verification, + }; + await ctx.db.patch(packageId, { + latestReleaseId: releaseId, + tags: { latest: releaseId }, + latestVersionSummary, + }); + const jobId = await ctx.db.insert("securityScanJobs", { + targetKind: "packageRelease", + packageReleaseId: releaseId, + status: "running", + source: "manual", + priority: 1, + hasMaliciousSignal: false, + waitForVtUntil: 0, + nextRunAt: 0, + attempts: 1, + leaseToken: "lease-one", + workerId: "endor-worker", + leaseExpiresAt: Date.now() + 60_000, + createdAt: 1, + updatedAt: 1, + }); + return { packageId, releaseId, jobId }; + }); + return { t, ...fixture }; +} + +async function readReleaseAndJob( + t: Awaited>["t"], + releaseId: Id<"packageReleases">, + jobId: Id<"securityScanJobs">, +) { + return await t.run(async (ctx) => ({ + release: await ctx.db.get(releaseId), + job: await ctx.db.get(jobId), + })); +} + +it("atomically stores a completed Endor summary and succeeds the package scan job", async () => { + const { t, releaseId, jobId } = await createPackageScanFixture(); + const endorAnalysis = { + status: "completed" as const, + checkedAt: 9, + reachableFunctionCount: 4, + findings: [{ severity: "high", summary: "Reachable vulnerable function" }], + }; + + await expect( + t.mutation(internal.packages.completeReleaseSecurityScanInternal, { + releaseId, + jobId, + leaseToken: "lease-one", + runId: "run-one", + llmAnalysis: { status: "clean", verdict: "benign", checkedAt: 10 }, + endorAnalysis, + }), + ).resolves.toEqual({ ok: true }); + + const { release, job } = await readReleaseAndJob(t, releaseId, jobId); + expect(release).toMatchObject({ + endorAnalysis, + llmAnalysis: { status: "clean", verdict: "benign", checkedAt: 10 }, + }); + expect(release).not.toHaveProperty("scannerReportsStorageId"); + expect(job).toMatchObject({ status: "succeeded", runId: "run-one" }); + expect(job).not.toHaveProperty("leaseToken"); + + const versionResponse = await t.fetch("/api/v1/packages/endor-plugin/versions/1.0.0"); + expect(versionResponse.status).toBe(200); + const versionBody = await versionResponse.json(); + expect(versionBody.version.endorAnalysis).toEqual(endorAnalysis); + + const securityResponse = await t.fetch("/api/v1/packages/endor-plugin/versions/1.0.0/security"); + expect(securityResponse.status).toBe(200); + const securityBody = await securityResponse.json(); + expect(securityBody.release.endorAnalysis).toEqual(endorAnalysis); +}); + +it("rejects stale status, lease, and target completions without writing scan results", async () => { + const { t, releaseId, jobId } = await createPackageScanFixture(); + const args = { + releaseId, + jobId, + leaseToken: "wrong-lease", + llmAnalysis: { status: "clean", checkedAt: 10 }, + endorAnalysis: { + status: "completed" as const, + checkedAt: 9, + reachableFunctionCount: 1, + findings: [], + }, + }; + + await expect( + t.mutation(internal.packages.completeReleaseSecurityScanInternal, args), + ).rejects.toThrow("Lease mismatch"); + + const otherReleaseId = await t.run(async (ctx) => { + const release = await ctx.db.get(releaseId); + if (!release) throw new Error("Fixture release missing"); + const { _id: _releaseId, _creationTime: _createdAt, ...fields } = release; + return await ctx.db.insert("packageReleases", { ...fields, version: "2.0.0" }); + }); + await expect( + t.mutation(internal.packages.completeReleaseSecurityScanInternal, { + ...args, + releaseId: otherReleaseId, + leaseToken: "lease-one", + }), + ).rejects.toThrow("Lease mismatch"); + + await t.run((ctx) => ctx.db.patch(jobId, { status: "queued" })); + await expect( + t.mutation(internal.packages.completeReleaseSecurityScanInternal, { + ...args, + leaseToken: "lease-one", + }), + ).rejects.toThrow("Lease mismatch"); + + const { release, job } = await readReleaseAndJob(t, releaseId, jobId); + expect(release).not.toHaveProperty("endorAnalysis"); + expect(release).not.toHaveProperty("llmAnalysis"); + expect(release).not.toHaveProperty("skillSpectorAnalysis"); + expect(await t.run((ctx) => ctx.db.get(otherReleaseId))).not.toHaveProperty("endorAnalysis"); + expect(job).toMatchObject({ status: "queued", leaseToken: "lease-one" }); +}); + +it("parses bounded Endor summaries before any package release write", async () => { + const { t, releaseId, jobId } = await createPackageScanFixture(); + + await expect( + t.mutation(internal.packages.completeReleaseSecurityScanInternal, { + releaseId, + jobId, + leaseToken: "lease-one", + llmAnalysis: { status: "clean", checkedAt: 10 }, + endorAnalysis: { + status: "completed", + checkedAt: 9, + reachableFunctionCount: 51, + findings: Array.from({ length: 51 }, (_, index) => ({ + severity: "high", + summary: `Finding ${index}`, + })), + }, + }), + ).rejects.toThrow(); + + const { release, job } = await readReleaseAndJob(t, releaseId, jobId); + expect(release).not.toHaveProperty("endorAnalysis"); + expect(release).not.toHaveProperty("llmAnalysis"); + expect(job).toMatchObject({ status: "running", leaseToken: "lease-one" }); +}); + +it("stores a failed Endor summary while enforcing a malicious primary verdict", async () => { + const { t, packageId, releaseId, jobId } = await createPackageScanFixture(); + const endorAnalysis = { + status: "failed" as const, + checkedAt: 11, + reason: "Endor dependency resolution failed", + }; + + await expect( + t.mutation(internal.packages.completeReleaseSecurityScanInternal, { + releaseId, + jobId, + leaseToken: "lease-one", + runId: "run-malicious", + llmAnalysis: { + status: "malicious", + verdict: "malicious", + confidence: "high", + summary: "Primary ClawScan found malicious behavior.", + checkedAt: 12, + }, + endorAnalysis, + }), + ).resolves.toEqual({ ok: true }); + + const { release, job } = await readReleaseAndJob(t, releaseId, jobId); + expect(release).toMatchObject({ + endorAnalysis, + llmAnalysis: { status: "malicious", verdict: "malicious" }, + verification: { scanStatus: "malicious" }, + softDeletedAt: expect.any(Number), + }); + expect(job).toMatchObject({ status: "succeeded", runId: "run-malicious" }); + expect(await t.run((ctx) => ctx.db.get(packageId))).toMatchObject({ + scanStatus: "malicious", + }); + + const versionResponse = await t.fetch("/api/v1/packages/endor-plugin/versions/1.0.0"); + expect(versionResponse.status).toBe(404); +}); diff --git a/convex/packages.ts b/convex/packages.ts index e805d94656..fe978127b0 100644 --- a/convex/packages.ts +++ b/convex/packages.ts @@ -68,6 +68,7 @@ import { buildPackageInspectorFindingsEmail, buildPackageInspectorValidationUrl, } from "./lib/emails"; +import { endorAnalysisSchema, endorAnalysisValidator } from "./lib/endorAnalysis"; import { experimentalClawsEnabled, isClawFamilyPubliclyVisible } from "./lib/experimentalClaws"; import { assertFeaturedCapacity } from "./lib/featuredPolicy"; import { orderPublishedFeatured, readPublishedFeaturedOrder } from "./lib/featuredSelections"; @@ -153,7 +154,7 @@ import { buildPackageInventoryDigest, hashSkillFiles } from "./lib/skills"; import { buildDeterministicPackageZip } from "./lib/skillZip"; import { PACKAGE_TRENDING_LEADERBOARD_KIND } from "./packageLeaderboards"; import { ACTIVE_PUBLISH_ATTEMPT_STATUSES, failedPublishAttemptPatch } from "./publishAttempts"; -import schema from "./schema"; +import schema, { llmAnalysisValidator } from "./schema"; const MAX_PUBLIC_LIST_PAGE_SIZE = 200; const MAX_PUBLIC_LIST_FILTER_SCAN_DOCUMENTS = 500; @@ -287,33 +288,6 @@ async function hasMissingPackageRecommendedScore( return Boolean(staleVersion); } -const llmAgenticRiskEvidenceValidator = v.object({ - path: v.string(), - snippet: v.string(), - explanation: v.string(), -}); - -const llmAgenticRiskFindingValidator = v.object({ - categoryId: v.string(), - categoryLabel: v.string(), - riskBucket: v.union( - v.literal("abnormal_behavior_control"), - v.literal("permission_boundary"), - v.literal("sensitive_data_protection"), - ), - status: v.union(v.literal("none"), v.literal("note"), v.literal("concern")), - severity: v.string(), - confidence: v.union(v.literal("high"), v.literal("medium"), v.literal("low")), - evidence: v.optional(llmAgenticRiskEvidenceValidator), - userImpact: v.string(), - recommendation: v.string(), -}); - -const llmRiskSummaryBucketValidator = v.object({ - status: v.union(v.literal("none"), v.literal("note"), v.literal("concern")), - summary: v.string(), - highestSeverity: v.optional(v.string()), -}); const packageOfficialMigrationPhaseValidator = v.union( v.literal("planned"), v.literal("published"), @@ -1358,6 +1332,7 @@ function toPublicPackageRelease(release: Doc<"packageReleases">, family: Package vtAnalysis: release.vtAnalysis, aigAnalysis: release.aigAnalysis, skillSpectorAnalysis: release.skillSpectorAnalysis, + endorAnalysis: release.endorAnalysis, llmAnalysis: release.llmAnalysis, staticScan: release.staticScan, createdAt: release.createdAt, @@ -3850,6 +3825,7 @@ export const listAuditPage = query({ version: latestRelease.version, createdAt: latestRelease.createdAt, vtAnalysis: latestRelease.vtAnalysis, + endorAnalysis: latestRelease.endorAnalysis, llmAnalysis: latestRelease.llmAnalysis, staticScan: latestRelease.staticScan ? { @@ -11653,7 +11629,6 @@ export const discardPendingPackagePublicationInternal = internalMutation({ if (typeof release?.clawpackStorageId === "string") { storageIds.add(release.clawpackStorageId as Id<"_storage">); } - if (release) await ctx.db.delete(release._id); await Promise.allSettled([...storageIds].map((storageId) => ctx.storage.delete(storageId))); @@ -12691,51 +12666,85 @@ export const updateReleaseAigAnalysisInternal = internalMutation({ }, }); -export const updateReleaseLlmAnalysisInternal = internalMutation({ +type ReleaseLlmAnalysis = NonNullable["llmAnalysis"]>; + +async function applyReleaseLlmAnalysis( + ctx: Pick & Partial>, + release: Doc<"packageReleases">, + llmAnalysis: ReleaseLlmAnalysis, +) { + await ctx.db.patch(release._id, { llmAnalysis }); + const updatedRelease: Doc<"packageReleases"> = { + ...release, + llmAnalysis, + }; + const llmVerdict = (llmAnalysis.verdict ?? llmAnalysis.status).trim().toLowerCase(); + await syncLatestPackageVerification(ctx, updatedRelease, { + quarantineMaliciousLatest: llmVerdict === "malicious", + maliciousTrigger: "malicious.llm_malicious", + }); +} + +export const completeReleaseSecurityScanInternal = internalMutation({ args: { releaseId: v.id("packageReleases"), - llmAnalysis: v.object({ - status: v.string(), - verdict: v.optional(v.string()), - confidence: v.optional(v.string()), - summary: v.optional(v.string()), - dimensions: v.optional( - v.array( - v.object({ - name: v.string(), - label: v.string(), - rating: v.string(), - detail: v.string(), - }), - ), - ), - guidance: v.optional(v.string()), - findings: v.optional(v.string()), - agenticRiskFindings: v.optional(v.array(llmAgenticRiskFindingValidator)), - riskSummary: v.optional( - v.object({ - abnormal_behavior_control: llmRiskSummaryBucketValidator, - permission_boundary: llmRiskSummaryBucketValidator, - sensitive_data_protection: llmRiskSummaryBucketValidator, - }), - ), - model: v.optional(v.string()), - checkedAt: v.number(), - }), + jobId: v.id("securityScanJobs"), + leaseToken: v.string(), + runId: v.optional(v.string()), + llmAnalysis: llmAnalysisValidator, + skillSpectorAnalysis: v.optional(skillSpectorAnalysisValidator), + endorAnalysis: v.optional(endorAnalysisValidator), }, handler: async (ctx, args) => { + const job = await ctx.db.get(args.jobId); + if ( + !job || + job.status !== "running" || + job.targetKind !== "packageRelease" || + job.packageReleaseId !== args.releaseId || + job.leaseToken !== args.leaseToken + ) { + throw new ConvexError("Lease mismatch"); + } const release = await ctx.db.get(args.releaseId); - if (!isReleaseActive(release)) return; - await ctx.db.patch(args.releaseId, { llmAnalysis: args.llmAnalysis }); - const updatedRelease = { + if (!isReleaseActive(release)) throw new ConvexError("Package release not found"); + const endorAnalysis = args.endorAnalysis + ? endorAnalysisSchema.parse(args.endorAnalysis) + : undefined; + + const releaseWithScannerResults: Doc<"packageReleases"> = { ...release, - llmAnalysis: args.llmAnalysis, - } as Doc<"packageReleases">; - const llmVerdict = (args.llmAnalysis.verdict ?? args.llmAnalysis.status).trim().toLowerCase(); - await syncLatestPackageVerification(ctx, updatedRelease, { - quarantineMaliciousLatest: llmVerdict === "malicious", - maliciousTrigger: "malicious.llm_malicious", + skillSpectorAnalysis: args.skillSpectorAnalysis, + endorAnalysis, + }; + await ctx.db.patch(args.releaseId, { + skillSpectorAnalysis: args.skillSpectorAnalysis, + endorAnalysis, }); + await applyReleaseLlmAnalysis(ctx, releaseWithScannerResults, args.llmAnalysis); + + const now = Date.now(); + await ctx.db.patch(args.jobId, { + status: "succeeded", + runId: args.runId, + completedAt: now, + leaseToken: undefined, + leaseExpiresAt: undefined, + updatedAt: now, + }); + return { ok: true as const }; + }, +}); + +export const updateReleaseLlmAnalysisInternal = internalMutation({ + args: { + releaseId: v.id("packageReleases"), + llmAnalysis: llmAnalysisValidator, + }, + handler: async (ctx, args) => { + const release = await ctx.db.get(args.releaseId); + if (!isReleaseActive(release)) return; + await applyReleaseLlmAnalysis(ctx, release, args.llmAnalysis); }, }); diff --git a/convex/schema.ts b/convex/schema.ts index ab1b509bcb..bc495b2928 100644 --- a/convex/schema.ts +++ b/convex/schema.ts @@ -7,6 +7,7 @@ import { canonicalTrendingSourceRefValidator, } from "./lib/canonicalTrending"; import { EMBEDDING_DIMENSIONS } from "./lib/embeddings"; +import { endorAnalysisValidator } from "./lib/endorAnalysis"; import { editorialSelection, featuredPublication } from "./lib/featuredSelections"; import { pluginCategoryClassificationValidator, @@ -151,7 +152,7 @@ const llmRiskSummaryBucketValidator = v.object({ highestSeverity: v.optional(v.string()), }); -const llmAnalysisValidator = v.object({ +export const llmAnalysisValidator = v.object({ status: v.string(), verdict: v.optional(v.string()), confidence: v.optional(v.string()), @@ -1922,36 +1923,8 @@ const packageReleases = defineTable({ vtAnalysis: v.optional(vtAnalysisValidator), aigAnalysis: v.optional(aigAnalysisValidator), skillSpectorAnalysis: v.optional(skillSpectorAnalysisValidator), - llmAnalysis: v.optional( - v.object({ - status: v.string(), - verdict: v.optional(v.string()), - confidence: v.optional(v.string()), - summary: v.optional(v.string()), - dimensions: v.optional( - v.array( - v.object({ - name: v.string(), - label: v.string(), - rating: v.string(), - detail: v.string(), - }), - ), - ), - guidance: v.optional(v.string()), - findings: v.optional(v.string()), - agenticRiskFindings: v.optional(v.array(llmAgenticRiskFindingValidator)), - riskSummary: v.optional( - v.object({ - abnormal_behavior_control: llmRiskSummaryBucketValidator, - permission_boundary: llmRiskSummaryBucketValidator, - sensitive_data_protection: llmRiskSummaryBucketValidator, - }), - ), - model: v.optional(v.string()), - checkedAt: v.number(), - }), - ), + endorAnalysis: v.optional(endorAnalysisValidator), + llmAnalysis: v.optional(llmAnalysisValidator), staticScan: v.optional( v.object({ status: v.union(v.literal("clean"), v.literal("suspicious"), v.literal("malicious")), diff --git a/convex/securityScan.test.ts b/convex/securityScan.test.ts index 52c02e7c39..7feba7959b 100644 --- a/convex/securityScan.test.ts +++ b/convex/securityScan.test.ts @@ -318,6 +318,14 @@ const completeCodexScanJobHandler = ( }>; checkedAt: number; }; + endorAnalysis?: + | { + status: "completed"; + checkedAt: number; + reachableFunctionCount: number; + findings: Array<{ severity: string; summary: string }>; + } + | { status: "skipped" | "failed"; checkedAt: number; reason: string }; scannerReportsStorageId?: string; runId?: string; }, @@ -585,6 +593,7 @@ const getStoredScanReportForUserInternalHandler = ( artifact: Record; report: { clawscan: Record | null; + endor: Record | null; skillspector: Record | null; staticAnalysis: Record | null; virustotal: Record | null; @@ -2394,6 +2403,12 @@ describe("securityScan", () => { summary: "Runs unexpected shell commands.", checkedAt: 1_700_000_000_000, }, + endorAnalysis: { + status: "completed", + checkedAt: 1_700_000_100_000, + reachableFunctionCount: 2, + findings: [{ severity: "high", summary: "Reachable vulnerable function" }], + }, createdAt: 1_700_000_000_000, }, }, @@ -2420,7 +2435,14 @@ describe("securityScan", () => { status: "malicious", summary: "Runs unexpected shell commands.", }, + endor: { + status: "completed", + checkedAt: 1_700_000_100_000, + reachableFunctionCount: 2, + findings: [{ severity: "high", summary: "Reachable vulnerable function" }], + }, }, + completedAt: 1_700_000_100_000, }); }); @@ -5698,7 +5720,7 @@ describe("securityScan", () => { expect(runMutation).toHaveBeenCalledTimes(2); }); - it("ignores package AIG payloads because plugin scanning is out of scope", async () => { + it("commits the package Endor summary without persisting package AIG payloads", async () => { vi.stubEnv("SECURITY_SCAN_WORKER_TOKEN", "worker-secret"); const runQuery = vi.fn(async () => ({ job: { @@ -5719,6 +5741,12 @@ describe("securityScan", () => { jobId: "securityScanJobs:plugin", leaseToken: "lease-token", llmAnalysis: { status: "clean", checkedAt: 123 }, + endorAnalysis: { + status: "completed", + checkedAt: 122, + reachableFunctionCount: 3, + findings: [{ severity: "high", summary: "Reachable finding" }], + }, aigAnalysis: { status: "error", issueCount: 99, @@ -5728,11 +5756,20 @@ describe("securityScan", () => { }, ); - expect(runMutation).toHaveBeenCalledTimes(4); + expect(runMutation).toHaveBeenCalledTimes(2); const releasePatches = runMutation.mock.calls .map(([, mutationArgs]) => mutationArgs as Record) .filter((mutationArgs) => mutationArgs.releaseId === "packageReleases:plugin"); - expect(releasePatches).toHaveLength(2); + expect(releasePatches).toHaveLength(1); + expect(releasePatches[0]).toMatchObject({ + jobId: "securityScanJobs:plugin", + leaseToken: "lease-token", + endorAnalysis: { + status: "completed", + reachableFunctionCount: 3, + }, + }); + expect(releasePatches[0]).not.toHaveProperty("scannerReportsStorageId"); expect(releasePatches).not.toContainEqual(expect.objectContaining({ aigAnalysis: undefined })); expect(releasePatches.some((patch) => "aigAnalysis" in patch)).toBe(false); }); diff --git a/convex/securityScan.ts b/convex/securityScan.ts index c281030873..abb388c197 100644 --- a/convex/securityScan.ts +++ b/convex/securityScan.ts @@ -6,6 +6,11 @@ import { action, internalAction, internalMutation, internalQuery, mutation } fro import { applyGitHubSkillVerificationResultHandler } from "./githubSkillSync"; import { assertAdmin, assertModerator, requireUser } from "./lib/access"; import { reusableAigAnalysis } from "./lib/aigAnalysis"; +import { + endorAnalysisSchema, + endorAnalysisValidator, + type EndorAnalysis, +} from "./lib/endorAnalysis"; import { Events, logEvent } from "./lib/observabilityEvents"; import { normalizePackageName } from "./lib/packageRegistry"; import { normalizePackageScanStatus } from "./lib/packageSecurity"; @@ -32,6 +37,7 @@ import { shouldPublishSkillsShCatalogEntry, } from "./lib/skillsShCatalogPublication"; import { redactWorkerPublicText } from "./lib/workerTextRedaction"; +import { llmAnalysisValidator } from "./schema"; import { requestSecurityScanDispatch } from "./securityScanDispatch"; const DEFAULT_VT_WAIT_MS = 10 * 60 * 1000; @@ -238,63 +244,6 @@ type EnqueuePackageReleaseScanArgs = { waitForVtMs?: number; }; -const llmAgenticRiskEvidenceValidator = v.object({ - path: v.string(), - snippet: v.string(), - explanation: v.string(), -}); - -const llmAgenticRiskFindingValidator = v.object({ - categoryId: v.string(), - categoryLabel: v.string(), - riskBucket: v.union( - v.literal("abnormal_behavior_control"), - v.literal("permission_boundary"), - v.literal("sensitive_data_protection"), - ), - status: v.union(v.literal("none"), v.literal("note"), v.literal("concern")), - severity: v.string(), - confidence: v.union(v.literal("high"), v.literal("medium"), v.literal("low")), - evidence: v.optional(llmAgenticRiskEvidenceValidator), - userImpact: v.string(), - recommendation: v.string(), -}); - -const llmRiskSummaryBucketValidator = v.object({ - status: v.union(v.literal("none"), v.literal("note"), v.literal("concern")), - summary: v.string(), - highestSeverity: v.optional(v.string()), -}); - -const llmAnalysisValidator = v.object({ - status: v.string(), - verdict: v.optional(v.string()), - confidence: v.optional(v.string()), - summary: v.optional(v.string()), - dimensions: v.optional( - v.array( - v.object({ - name: v.string(), - label: v.string(), - rating: v.string(), - detail: v.string(), - }), - ), - ), - guidance: v.optional(v.string()), - findings: v.optional(v.string()), - agenticRiskFindings: v.optional(v.array(llmAgenticRiskFindingValidator)), - riskSummary: v.optional( - v.object({ - abnormal_behavior_control: llmRiskSummaryBucketValidator, - permission_boundary: llmRiskSummaryBucketValidator, - sensitive_data_protection: llmRiskSummaryBucketValidator, - }), - ), - model: v.optional(v.string()), - checkedAt: v.number(), -}); - const skillSpectorIssueValidator = v.object({ issueId: v.string(), category: v.optional(v.string()), @@ -388,6 +337,7 @@ const catalogScanVerdictValidator = v.union( const internalRefs = internal as unknown as { packages: { + completeReleaseSecurityScanInternal: unknown; getPackageByIdInternal: unknown; getReleaseByIdInternal: unknown; updateReleaseLlmAnalysisInternal: unknown; @@ -1329,15 +1279,18 @@ function skillScanReportFromRequest(request: Doc<"skillScanRequests">) { }; } -function storedScanReportFromArtifact( - artifact: Pick< - Doc<"skillVersions"> | Doc<"packageReleases">, - "aigAnalysis" | "llmAnalysis" | "skillSpectorAnalysis" | "staticScan" | "vtAnalysis" - >, -) { +type StoredScanArtifact = Pick< + Doc<"skillVersions"> | Doc<"packageReleases">, + "aigAnalysis" | "llmAnalysis" | "skillSpectorAnalysis" | "staticScan" | "vtAnalysis" +> & { + endorAnalysis?: EndorAnalysis; +}; + +function storedScanReportFromArtifact(artifact: StoredScanArtifact) { return { aig: artifact.aigAnalysis ?? null, clawscan: artifact.llmAnalysis ?? null, + endor: artifact.endorAnalysis ?? null, skillspector: artifact.skillSpectorAnalysis ?? null, staticAnalysis: artifact.staticScan ?? null, virustotal: artifact.vtAnalysis @@ -1349,30 +1302,22 @@ function storedScanReportFromArtifact( }; } -function hasStoredScanReport( - artifact: Pick< - Doc<"skillVersions"> | Doc<"packageReleases">, - "aigAnalysis" | "llmAnalysis" | "skillSpectorAnalysis" | "staticScan" | "vtAnalysis" - >, -) { +function hasStoredScanReport(artifact: StoredScanArtifact) { return Boolean( artifact.aigAnalysis || artifact.llmAnalysis || + artifact.endorAnalysis || artifact.skillSpectorAnalysis || artifact.staticScan || artifact.vtAnalysis, ); } -function completedAtFromStoredScanReport( - artifact: Pick< - Doc<"skillVersions"> | Doc<"packageReleases">, - "aigAnalysis" | "llmAnalysis" | "skillSpectorAnalysis" | "staticScan" | "vtAnalysis" - >, -) { +function completedAtFromStoredScanReport(artifact: StoredScanArtifact) { const checkedAtValues = [ artifact.aigAnalysis?.checkedAt, artifact.llmAnalysis?.checkedAt, + artifact.endorAnalysis?.checkedAt, artifact.skillSpectorAnalysis?.checkedAt, artifact.staticScan?.checkedAt, artifact.vtAnalysis?.checkedAt, @@ -4150,6 +4095,7 @@ export const completeCodexScanJob = action({ llmAnalysis: llmAnalysisValidator, aigAnalysis: v.optional(aigAnalysisValidator), skillSpectorAnalysis: v.optional(skillSpectorAnalysisValidator), + endorAnalysis: v.optional(endorAnalysisValidator), scannerReportsStorageId: v.optional(v.id("_storage")), runId: v.optional(v.string()), }, @@ -4170,6 +4116,12 @@ export const completeCodexScanJob = action({ if (!isCatalogScanRequest && target.job.leaseToken !== args.leaseToken) { throw new ConvexError("Lease mismatch"); } + const endorAnalysis: EndorAnalysis | undefined = args.endorAnalysis + ? endorAnalysisSchema.parse(args.endorAnalysis) + : undefined; + if (endorAnalysis && target.job.targetKind !== "packageRelease") { + throw new ConvexError("Endor analysis is only supported for package release scans"); + } const completedAigAnalysis = reusableAigAnalysis(args.aigAnalysis); if ( (target.job.targetKind === "skillVersion" || target.job.targetKind === "skillScanRequest") && @@ -4207,16 +4159,29 @@ export const completeCodexScanJob = action({ if (target.job.targetKind === "skillVersion" && target.version) { await updateSkillVersion(target.version._id); } else if (target.job.targetKind === "packageRelease" && target.release) { - await runMutationRef(ctx, internalRefs.packages.updateReleaseSkillSpectorAnalysisInternal, { - releaseId: target.release._id, - ...(args.skillSpectorAnalysis - ? { skillSpectorAnalysis: capSkillSpectorAnalysisForStorage(args.skillSpectorAnalysis) } - : {}), - }); - await runMutationRef(ctx, internalRefs.packages.updateReleaseLlmAnalysisInternal, { - releaseId: target.release._id, - llmAnalysis: args.llmAnalysis, - }); + const result = await runMutationRef<{ ok: true }>( + ctx, + internalRefs.packages.completeReleaseSecurityScanInternal, + { + releaseId: target.release._id, + jobId: args.jobId, + leaseToken: args.leaseToken, + runId: args.runId, + llmAnalysis: args.llmAnalysis, + ...(args.skillSpectorAnalysis + ? { + skillSpectorAnalysis: capSkillSpectorAnalysisForStorage(args.skillSpectorAnalysis), + } + : {}), + endorAnalysis, + }, + ); + await runMutationRef( + ctx, + internalRefs.securityScanDispatch.requestSecurityScanDispatchInternal, + {}, + ); + return result; } else if (target.job.targetKind === "skillScanRequest" && target.scanRequest) { let writtenBack = false; if ( diff --git a/packages/clawhub/src/schema/packages.ts b/packages/clawhub/src/schema/packages.ts index ab8383c5ce..5a0bb6f16f 100644 --- a/packages/clawhub/src/schema/packages.ts +++ b/packages/clawhub/src/schema/packages.ts @@ -183,6 +183,28 @@ export const PackageSkillSpectorAnalysisSchema = type({ }); export type PackageSkillSpectorAnalysis = (typeof PackageSkillSpectorAnalysisSchema)[inferred]; +const PackageEndorFindingSeveritySchema = type("string").atMostLength(64); +const PackageEndorFindingSummarySchema = type("string").atMostLength(2000); +const PackageEndorFindingSchema = type({ + severity: PackageEndorFindingSeveritySchema, + summary: PackageEndorFindingSummarySchema, +}); +const PackageEndorCompletedAnalysisSchema = type({ + status: '"completed"', + checkedAt: "number", + reachableFunctionCount: "number.integer >= 0", + findings: PackageEndorFindingSchema.array().atMostLength(50), +}); +const PackageEndorIncompleteAnalysisSchema = type({ + status: '"skipped"|"failed"', + checkedAt: "number", + reason: type("string").atMostLength(2000), +}); +export const PackageEndorAnalysisSchema = PackageEndorCompletedAnalysisSchema.or( + PackageEndorIncompleteAnalysisSchema, +); +export type PackageEndorAnalysis = (typeof PackageEndorAnalysisSchema)[inferred]; + export const PackageAigFindingSchema = type({ ruleId: "string", level: "string", @@ -523,6 +545,7 @@ export const ApiV1PackageVersionResponseSchema = type({ sha256hash: "string|null?", vtAnalysis: PackageVtAnalysisSchema.or("null").optional(), skillSpectorAnalysis: PackageSkillSpectorAnalysisSchema.or("null").optional(), + endorAnalysis: PackageEndorAnalysisSchema.or("null").optional(), aigAnalysis: PackageAigAnalysisSchema.or("null").optional(), llmAnalysis: PackageLlmAnalysisSchema.or("null").optional(), staticScan: PackageStaticScanSchema.or("null").optional(), @@ -577,6 +600,7 @@ export const ApiV1PackageSecurityResponseSchema = type({ npmShasum: "string?", npmTarballName: "string?", createdAt: "number", + endorAnalysis: PackageEndorAnalysisSchema.or("null").optional(), }), trust: type({ scanStatus: '"clean"|"suspicious"|"malicious"|"pending"|"not-run"', diff --git a/packages/schema/dist/packages.d.ts b/packages/schema/dist/packages.d.ts index d86676885c..88e730fdc8 100644 --- a/packages/schema/dist/packages.d.ts +++ b/packages/schema/dist/packages.d.ts @@ -200,6 +200,20 @@ export declare const PackageSkillSpectorAnalysisSchema: import("arktype/internal checkedAt: number; }, {}>; export type PackageSkillSpectorAnalysis = (typeof PackageSkillSpectorAnalysisSchema)[inferred]; +export declare const PackageEndorAnalysisSchema: import("arktype/internal/variants/object.ts").ObjectType<{ + status: "completed"; + checkedAt: number; + reachableFunctionCount: number; + findings: { + severity: string; + summary: string; + }[]; +} | { + status: "failed" | "skipped"; + checkedAt: number; + reason: string; +}, {}>; +export type PackageEndorAnalysis = (typeof PackageEndorAnalysisSchema)[inferred]; export declare const PackageAigFindingSchema: import("arktype/internal/variants/object.ts").ObjectType<{ ruleId: string; level: string; @@ -861,6 +875,19 @@ export declare const ApiV1PackageVersionResponseSchema: import("arktype/internal error?: string | undefined; checkedAt: number; } | null | undefined; + endorAnalysis?: { + status: "completed"; + checkedAt: number; + reachableFunctionCount: number; + findings: { + severity: string; + summary: string; + }[]; + } | { + status: "failed" | "skipped"; + checkedAt: number; + reason: string; + } | null | undefined; aigAnalysis?: { status: string; issueCount: number; @@ -962,6 +989,19 @@ export declare const ApiV1PackageSecurityResponseSchema: import("arktype/interna npmShasum?: string | undefined; npmTarballName?: string | undefined; createdAt: number; + endorAnalysis?: { + status: "completed"; + checkedAt: number; + reachableFunctionCount: number; + findings: { + severity: string; + summary: string; + }[]; + } | { + status: "failed" | "skipped"; + checkedAt: number; + reason: string; + } | null | undefined; }; trust: { scanStatus: "clean" | "malicious" | "not-run" | "pending" | "suspicious"; @@ -1200,6 +1240,19 @@ export declare const ApiV1PluginDetailResponseSchema: import("arktype/internal/v error?: string | undefined; checkedAt: number; } | null | undefined; + endorAnalysis?: { + status: "completed"; + checkedAt: number; + reachableFunctionCount: number; + findings: { + severity: string; + summary: string; + }[]; + } | { + status: "failed" | "skipped"; + checkedAt: number; + reason: string; + } | null | undefined; aigAnalysis?: { status: string; issueCount: number; @@ -1272,6 +1325,19 @@ export declare const ApiV1PluginDetailResponseSchema: import("arktype/internal/v npmShasum?: string | undefined; npmTarballName?: string | undefined; createdAt: number; + endorAnalysis?: { + status: "completed"; + checkedAt: number; + reachableFunctionCount: number; + findings: { + severity: string; + summary: string; + }[]; + } | { + status: "failed" | "skipped"; + checkedAt: number; + reason: string; + } | null | undefined; }; trust: { scanStatus: "clean" | "malicious" | "not-run" | "pending" | "suspicious"; diff --git a/packages/schema/dist/packages.js b/packages/schema/dist/packages.js index 541e6c8b09..9b8af7e40b 100644 --- a/packages/schema/dist/packages.js +++ b/packages/schema/dist/packages.js @@ -179,6 +179,24 @@ export const PackageSkillSpectorAnalysisSchema = type({ error: "string?", checkedAt: "number", }); +const PackageEndorFindingSeveritySchema = type("string").atMostLength(64); +const PackageEndorFindingSummarySchema = type("string").atMostLength(2000); +const PackageEndorFindingSchema = type({ + severity: PackageEndorFindingSeveritySchema, + summary: PackageEndorFindingSummarySchema, +}); +const PackageEndorCompletedAnalysisSchema = type({ + status: '"completed"', + checkedAt: "number", + reachableFunctionCount: "number.integer >= 0", + findings: PackageEndorFindingSchema.array().atMostLength(50), +}); +const PackageEndorIncompleteAnalysisSchema = type({ + status: '"skipped"|"failed"', + checkedAt: "number", + reason: type("string").atMostLength(2000), +}); +export const PackageEndorAnalysisSchema = PackageEndorCompletedAnalysisSchema.or(PackageEndorIncompleteAnalysisSchema); export const PackageAigFindingSchema = type({ ruleId: "string", level: "string", @@ -476,6 +494,7 @@ export const ApiV1PackageVersionResponseSchema = type({ sha256hash: "string|null?", vtAnalysis: PackageVtAnalysisSchema.or("null").optional(), skillSpectorAnalysis: PackageSkillSpectorAnalysisSchema.or("null").optional(), + endorAnalysis: PackageEndorAnalysisSchema.or("null").optional(), aigAnalysis: PackageAigAnalysisSchema.or("null").optional(), llmAnalysis: PackageLlmAnalysisSchema.or("null").optional(), staticScan: PackageStaticScanSchema.or("null").optional(), @@ -527,6 +546,7 @@ export const ApiV1PackageSecurityResponseSchema = type({ npmShasum: "string?", npmTarballName: "string?", createdAt: "number", + endorAnalysis: PackageEndorAnalysisSchema.or("null").optional(), }), trust: type({ scanStatus: '"clean"|"suspicious"|"malicious"|"pending"|"not-run"', diff --git a/packages/schema/dist/packages.js.map b/packages/schema/dist/packages.js.map index 3cb3a7d236..88f54336b6 100644 --- a/packages/schema/dist/packages.js.map +++ b/packages/schema/dist/packages.js.map @@ -1 +1 @@ -{"version":3,"file":"packages.js","sourceRoot":"","sources":["../src/packages.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"packages.js","sourceRoot":"","sources":["../src/packages.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/packages/schema/src/packages.ts b/packages/schema/src/packages.ts index 0613fb57e9..0d2674d98f 100644 --- a/packages/schema/src/packages.ts +++ b/packages/schema/src/packages.ts @@ -242,6 +242,28 @@ export const PackageSkillSpectorAnalysisSchema = type({ }); export type PackageSkillSpectorAnalysis = (typeof PackageSkillSpectorAnalysisSchema)[inferred]; +const PackageEndorFindingSeveritySchema = type("string").atMostLength(64); +const PackageEndorFindingSummarySchema = type("string").atMostLength(2000); +const PackageEndorFindingSchema = type({ + severity: PackageEndorFindingSeveritySchema, + summary: PackageEndorFindingSummarySchema, +}); +const PackageEndorCompletedAnalysisSchema = type({ + status: '"completed"', + checkedAt: "number", + reachableFunctionCount: "number.integer >= 0", + findings: PackageEndorFindingSchema.array().atMostLength(50), +}); +const PackageEndorIncompleteAnalysisSchema = type({ + status: '"skipped"|"failed"', + checkedAt: "number", + reason: type("string").atMostLength(2000), +}); +export const PackageEndorAnalysisSchema = PackageEndorCompletedAnalysisSchema.or( + PackageEndorIncompleteAnalysisSchema, +); +export type PackageEndorAnalysis = (typeof PackageEndorAnalysisSchema)[inferred]; + export const PackageAigFindingSchema = type({ ruleId: "string", level: "string", @@ -615,6 +637,7 @@ export const ApiV1PackageVersionResponseSchema = type({ sha256hash: "string|null?", vtAnalysis: PackageVtAnalysisSchema.or("null").optional(), skillSpectorAnalysis: PackageSkillSpectorAnalysisSchema.or("null").optional(), + endorAnalysis: PackageEndorAnalysisSchema.or("null").optional(), aigAnalysis: PackageAigAnalysisSchema.or("null").optional(), llmAnalysis: PackageLlmAnalysisSchema.or("null").optional(), staticScan: PackageStaticScanSchema.or("null").optional(), @@ -670,6 +693,7 @@ export const ApiV1PackageSecurityResponseSchema = type({ npmShasum: "string?", npmTarballName: "string?", createdAt: "number", + endorAnalysis: PackageEndorAnalysisSchema.or("null").optional(), }), trust: type({ scanStatus: '"clean"|"suspicious"|"malicious"|"pending"|"not-run"', diff --git a/scripts/lib/runWorkerCommand.ts b/scripts/lib/runWorkerCommand.ts new file mode 100644 index 0000000000..1388f686ff --- /dev/null +++ b/scripts/lib/runWorkerCommand.ts @@ -0,0 +1,105 @@ +import { spawn } from "node:child_process"; + +export class CommandFailure extends Error { + exitCode: number | null; + stderr: string; + stdout: string; + timedOut: boolean; + + constructor( + message: string, + exitCode: number | null, + stdout: string, + stderr: string, + timedOut: boolean, + ) { + super(message); + this.name = "CommandFailure"; + this.exitCode = exitCode; + this.stdout = stdout; + this.stderr = stderr; + this.timedOut = timedOut; + } +} + +export async function runWorkerCommand( + command: string, + args: string[], + options: { + commandLabel?: string; + cwd: string; + env: NodeJS.ProcessEnv; + input?: string; + timeoutMs: number; + }, +) { + return await new Promise<{ stdout: string; stderr: string }>((resolvePromise, reject) => { + const child = spawn(command, args, { + cwd: options.cwd, + detached: process.platform !== "win32", + env: options.env, + stdio: ["pipe", "pipe", "pipe"], + }); + let stdout = ""; + let stderr = ""; + let timedOut = false; + let settled = false; + let forceKillTimeout: NodeJS.Timeout | undefined; + const killProcessTree = (signal: NodeJS.Signals) => { + if (process.platform !== "win32" && child.pid) { + try { + process.kill(-child.pid, signal); + return; + } catch { + // The process group may already have exited; fall back to the direct child. + } + } + child.kill(signal); + }; + const timeout = setTimeout(() => { + timedOut = true; + killProcessTree("SIGTERM"); + forceKillTimeout = setTimeout(() => killProcessTree("SIGKILL"), 10_000); + forceKillTimeout.unref(); + }, options.timeoutMs); + const clearTimers = () => { + clearTimeout(timeout); + if (forceKillTimeout) clearTimeout(forceKillTimeout); + }; + child.stdout.on("data", (chunk) => { + stdout += String(chunk); + }); + child.stderr.on("data", (chunk) => { + stderr += String(chunk); + }); + child.on("error", (error) => { + if (settled) return; + settled = true; + clearTimers(); + if (timedOut) killProcessTree("SIGKILL"); + reject(error); + }); + child.on("close", (code) => { + if (settled) return; + settled = true; + clearTimers(); + if (timedOut) killProcessTree("SIGKILL"); + if (code === 0 && !timedOut) { + resolvePromise({ stdout, stderr }); + return; + } + const label = options.commandLabel ?? command; + reject( + new CommandFailure( + `${label} ${timedOut ? "timed out" : `exited ${code}`}; see redacted stdout/stderr diagnostics`, + code, + stdout, + stderr, + timedOut, + ), + ); + }); + if (options.input) child.stdin.end(options.input); + else child.stdin.end(); + }); +} diff --git a/scripts/lib/workerRedaction.test.ts b/scripts/lib/workerRedaction.test.ts index 9ee356db55..a0a761d7c0 100644 --- a/scripts/lib/workerRedaction.test.ts +++ b/scripts/lib/workerRedaction.test.ts @@ -191,6 +191,9 @@ describe("worker transport redaction", () => { maskKnownWorkerSecrets( { GITHUB_ACTIONS: "true", + ENDOR_API_CREDENTIALS_KEY: "endor-key-secret", + ENDOR_API_CREDENTIALS_SECRET: "endor-credential-secret", + ENDOR_TOKEN: "endor-token-secret", OPENAI_API_KEY: "sk-runtime-secret", SECURITY_SCAN_WORKER_TOKEN: "worker-token-secret", } as NodeJS.ProcessEnv, @@ -199,5 +202,8 @@ describe("worker transport redaction", () => { expect(lines).toContain("::add-mask::sk-runtime-secret\n"); expect(lines).toContain("::add-mask::worker-token-secret\n"); + expect(lines).toContain("::add-mask::endor-token-secret\n"); + expect(lines).toContain("::add-mask::endor-key-secret\n"); + expect(lines).toContain("::add-mask::endor-credential-secret\n"); }); }); diff --git a/scripts/lib/workerRedaction.ts b/scripts/lib/workerRedaction.ts index 13ce7aad32..997a5fcefe 100644 --- a/scripts/lib/workerRedaction.ts +++ b/scripts/lib/workerRedaction.ts @@ -54,6 +54,9 @@ export function maskKnownWorkerSecrets( "GITHUB_TOKEN", "CONVEX_DEPLOY_KEY", "HOMEBREW_GITHUB_API_TOKEN", + "ENDOR_TOKEN", + "ENDOR_API_CREDENTIALS_KEY", + "ENDOR_API_CREDENTIALS_SECRET", ]; for (const key of secretKeys) { maskGitHubActionsSecret(env[key], { env, write }); diff --git a/scripts/security/clawScanTarget.ts b/scripts/security/clawScanTarget.ts new file mode 100644 index 0000000000..eea2a14deb --- /dev/null +++ b/scripts/security/clawScanTarget.ts @@ -0,0 +1,20 @@ +import { lstat } from "node:fs/promises"; +import { join } from "node:path"; + +export async function resolveClawScanTargetForRoot(input: { + artifactKind: "packageRelease" | "skill"; + root: string; + workspace: string; +}) { + const manifests = ["SKILL.md", "openclaw.plugin.json"] as const; + const present = await Promise.all( + manifests.map(async (name) => + (await lstat(join(input.workspace, input.root, name)).catch(() => null))?.isFile(), + ), + ); + // ClawScan rejects dual-manifest directories. An explicit manifest selects the + // claimed kind while ClawScan still scans the entire dual-layout directory. + return present.every(Boolean) + ? `${input.root}/${manifests[input.artifactKind === "packageRelease" ? 1 : 0]}` + : input.root; +} diff --git a/scripts/security/endor/Dockerfile b/scripts/security/endor/Dockerfile new file mode 100644 index 0000000000..a008a7c398 --- /dev/null +++ b/scripts/security/endor/Dockerfile @@ -0,0 +1,22 @@ +FROM node:22.18.0-bookworm@sha256:3266bc9e8bee1acc8a77386eefaf574987d2729b8c5ec35b0dbd6ddbc40b0ce2 + +ARG ENDORCTL_SHA256 +ARG TYPESCRIPT_VERSION=6.0.3 + +# Supply the reviewed Linux endorctl binary as "endorctl" in the build context. +COPY endorctl /usr/local/bin/endorctl +RUN test -n "$ENDORCTL_SHA256" \ + && printf '%s %s\n' "$ENDORCTL_SHA256" /usr/local/bin/endorctl | sha256sum --check - \ + && chmod 755 /usr/local/bin/endorctl \ + && npm install --global --ignore-scripts --no-audit --no-fund npm@11.19.1 \ + && npm install --global --ignore-scripts --no-audit --no-fund "typescript@${TYPESCRIPT_VERSION}" + +ENV NPM_CONFIG_IGNORE_SCRIPTS=true \ + NPM_CONFIG_AUDIT=false \ + NPM_CONFIG_FUND=false \ + COREPACK_ENABLE_PROJECT_SPEC=0 + +COPY scan.sh /usr/local/bin/clawhub-endor-scan +RUN chmod 755 /usr/local/bin/clawhub-endor-scan + +WORKDIR /workspace diff --git a/scripts/security/endor/fixtures/probe_server.py b/scripts/security/endor/fixtures/probe_server.py new file mode 100644 index 0000000000..3d6afb1665 --- /dev/null +++ b/scripts/security/endor/fixtures/probe_server.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +import hashlib +import http.server +import json +import pathlib +import sys +import threading + + +events_path = pathlib.Path(sys.argv[1]) +port_path = pathlib.Path(sys.argv[2]) +tarball_path = pathlib.Path(sys.argv[3]) +expected_canaries = sys.argv[4:] + + +class Handler(http.server.BaseHTTPRequestHandler): + def log_message(self, _format, *_args): + return + + def do_GET(self): + authorization = self.headers.get("Authorization", "") + event = { + "path": self.path, + "authorization_present": bool(authorization), + "authorization_sha256": hashlib.sha256(authorization.encode()).hexdigest() + if authorization + else None, + "contains_expected_synthetic_canary": any( + canary in authorization for canary in expected_canaries + ) if authorization else False, + } + with events_path.open("a") as events: + events.write(json.dumps(event, sort_keys=True) + "\n") + + if self.path == "/credential-probe-pkg": + host = self.headers["Host"] + tarball = tarball_path.read_bytes() + body = json.dumps( + { + "name": "credential-probe-pkg", + "dist-tags": {"latest": "1.0.0"}, + "versions": { + "1.0.0": { + "_id": "credential-probe-pkg@1.0.0", + "name": "credential-probe-pkg", + "version": "1.0.0", + "dist": { + "tarball": f"http://{host}/credential-probe-pkg/-/credential-probe-pkg-1.0.0.tgz", + "shasum": hashlib.sha1(tarball).hexdigest(), + }, + } + }, + } + ).encode() + self.send_response(200) + self.send_header("Content-Type", "application/json") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + return + + if self.path == "/credential-probe-pkg/-/credential-probe-pkg-1.0.0.tgz": + body = tarball_path.read_bytes() + self.send_response(200) + self.send_header("Content-Type", "application/octet-stream") + self.send_header("Content-Length", str(len(body))) + self.end_headers() + self.wfile.write(body) + return + + self.send_response(404) + self.end_headers() + + +server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), Handler) +port_path.write_text(str(server.server_address[1]) + "\n") +threading.Thread(target=server.serve_forever, daemon=False).start() diff --git a/scripts/security/endor/fixtures/run.sh b/scripts/security/endor/fixtures/run.sh new file mode 100644 index 0000000000..dd744a2411 --- /dev/null +++ b/scripts/security/endor/fixtures/run.sh @@ -0,0 +1,186 @@ +#!/bin/sh +set -eu + +scan_script=$1 +work=$(mktemp -d) +mkdir -p "$work/bin" "$work/source" +printf '%s\n' '{"name":"endor-fixture","private":true}' > "$work/source/package.json" +printf '%s\n' '# Endor fixture' > "$work/source/SKILL.md" + +cat > "$work/bin/endorctl" <<'ENDOR_FIXTURE' +#!/bin/sh +set -eu + +test "$ENDOR_NAMESPACE" = synthetic-namespace +test "$ENDOR_TOKEN" = synthetic-endor-token +test "$ENDOR_API_CREDENTIALS_KEY" = synthetic-endor-key +test "$ENDOR_API_CREDENTIALS_SECRET" = synthetic-endor-secret +test "$VIRUSTOTAL_API_KEY" = synthetic-virustotal-key +test "$UNKNOWN_ADDITIONAL_SECRET" = synthetic-unknown-secret + +probe=$(mktemp -d) +mkdir -p \ + "$probe/tar/package" \ + "$probe/gitdep" \ + "$probe/git-root" \ + "$probe/malicious-gitdep" \ + "$probe/malicious-git-root" \ + "$probe/npm-root" \ + "$probe/npm-gitdep" \ + "$probe/npm-git-root" \ + "$probe/malicious-npm-root" +printf '%s\n' '{"name":"credential-probe-pkg","version":"1.0.0"}' > "$probe/tar/package/package.json" +tar -C "$probe/tar" -czf "$probe/credential-probe-pkg-1.0.0.tgz" package +python3 /fixture/probe_server.py \ + "$ENDOR_FIXTURE_OUTPUT/events.jsonl" \ + "$ENDOR_FIXTURE_OUTPUT/port" \ + "$probe/credential-probe-pkg-1.0.0.tgz" \ + "synthetic-endor-token" \ + "synthetic-endor-key" \ + "synthetic-endor-secret" \ + "synthetic-virustotal-key" \ + "synthetic-unknown-secret" & +server_pid=$! +cleanup_server() { + kill "$server_pid" 2>/dev/null || true + wait "$server_pid" 2>/dev/null || true +} +trap cleanup_server EXIT +while test ! -s "$ENDOR_FIXTURE_OUTPUT/port"; do sleep 0.05; done +port=$(cat "$ENDOR_FIXTURE_OUTPUT/port") + +cat > "$probe/gitdep/package.json" <<'PACKAGE_JSON' +{"name":"git-prepare-dep","version":"1.0.0","scripts":{"prepare":"node prepare.js"},"dependencies":{"credential-probe-pkg":"1.0.0"}} +PACKAGE_JSON +cat > "$probe/gitdep/prepare.js" <<'PREPARE_SCRIPT' +const crypto = require('crypto'); +const fs = require('fs'); +const canaries = [ + process.env.ENDOR_TOKEN, + process.env.VIRUSTOTAL_API_KEY, + process.env.UNKNOWN_ADDITIONAL_SECRET, +].filter(Boolean); +fs.writeFileSync('/output/prepare.json', JSON.stringify({ + executed: true, + synthetic_canary_present: canaries.length > 0, + synthetic_canary_sha256: canaries.map((value) => crypto.createHash('sha256').update(value).digest('hex')), +}) + '\n'); +PREPARE_SCRIPT +cat > "$probe/gitdep/.npmrc" < "$probe/git-root/package.json" < "$ENDOR_FIXTURE_OUTPUT/yarn-benign.stdout" 2> "$ENDOR_FIXTURE_OUTPUT/yarn-benign.stderr" +test ! -e "$ENDOR_FIXTURE_OUTPUT/prepare.json" +test -f "$probe/git-root/node_modules/credential-probe-pkg/package.json" + +cat > "$probe/malicious-gitdep/package.json" <<'PACKAGE_JSON' +{"name":"malicious-git-prepare-dep","version":"1.0.0","scripts":{"prepare":"node prepare.js"},"dependencies":{"credential-probe-pkg":"1.0.0"}} +PACKAGE_JSON +cp "$probe/gitdep/prepare.js" "$probe/malicious-gitdep/prepare.js" +cat > "$probe/malicious-gitdep/.npmrc" < "$probe/malicious-git-root/package.json" < "$ENDOR_FIXTURE_OUTPUT/yarn-malicious.stdout" 2> "$ENDOR_FIXTURE_OUTPUT/yarn-malicious.stderr"; then + echo "Yarn accepted a dependency config with a missing credential variable" >&2 + exit 1 +fi +test ! -e "$ENDOR_FIXTURE_OUTPUT/prepare.json" + +cat > "$probe/npm-root/package.json" <<'NPM_ROOT_PACKAGE' +{"name":"npm-probe-root","private":true,"dependencies":{"credential-probe-pkg":"1.0.0"}} +NPM_ROOT_PACKAGE +( + cd "$probe/npm-root" + npm install --registry "http://127.0.0.1:$port/" --ignore-scripts --cache "$probe/npm-cache" --no-audit --no-fund +) > "$ENDOR_FIXTURE_OUTPUT/npm-benign.stdout" 2> "$ENDOR_FIXTURE_OUTPUT/npm-benign.stderr" +test -f "$probe/npm-root/node_modules/credential-probe-pkg/package.json" + +cat > "$probe/malicious-npm-root/package.json" <<'NPM_ROOT_PACKAGE' +{"name":"malicious-npm-probe-root","private":true,"dependencies":{"credential-probe-pkg":"1.0.0"}} +NPM_ROOT_PACKAGE +cat > "$probe/malicious-npm-root/.npmrc" < "$ENDOR_FIXTURE_OUTPUT/npm-malicious.stdout" 2> "$ENDOR_FIXTURE_OUTPUT/npm-malicious.stderr" +test -f "$probe/malicious-npm-root/node_modules/credential-probe-pkg/package.json" + +cat > "$probe/npm-gitdep/package.json" <<'NPM_GIT_PACKAGE' +{"name":"npm-git-prepare-dep","version":"1.0.0","scripts":{"prepare":"node prepare.js"},"dependencies":{"credential-probe-pkg":"1.0.0"}} +NPM_GIT_PACKAGE +cat > "$probe/npm-gitdep/prepare.js" <<'NPM_GIT_PREPARE' +const fs = require('fs'); +fs.writeFileSync('/output/npm-git-prepare.json', JSON.stringify({executed: true}) + '\n'); +NPM_GIT_PREPARE +cat > "$probe/npm-gitdep/.npmrc" < "$probe/npm-git-root/package.json" < "$ENDOR_FIXTURE_OUTPUT/npm-git.stdout" 2> "$ENDOR_FIXTURE_OUTPUT/npm-git.stderr" +test -f "$probe/npm-git-root/node_modules/npm-git-prepare-dep/package.json" +test -f "$probe/npm-git-root/node_modules/credential-probe-pkg/package.json" +if test -e "$ENDOR_FIXTURE_OUTPUT/npm-git-prepare.json"; then + echo "npm Git dependency prepare executed despite ignore-scripts" >&2 + exit 1 +fi + +python3 - "$ENDOR_FIXTURE_OUTPUT/events.jsonl" <<'VERIFY_EVENTS' +import json +import pathlib +import sys + +events = [json.loads(line) for line in pathlib.Path(sys.argv[1]).read_text().splitlines()] +paths = [event["path"] for event in events] +assert paths.count("/credential-probe-pkg") >= 4, events +assert paths.count("/credential-probe-pkg/-/credential-probe-pkg-1.0.0.tgz") >= 4, events +assert not any(event["contains_expected_synthetic_canary"] for event in events), events +VERIFY_EVENTS +printf '%s\n' '{"all_findings":[],"blocking_findings":[],"warning_findings":[]}' +ENDOR_FIXTURE +chmod 755 "$work/bin/endorctl" + +export PATH="$work/bin:$PATH" +export ENDOR_NAMESPACE=synthetic-namespace +export ENDOR_TOKEN=synthetic-endor-token +export ENDOR_API_CREDENTIALS_KEY=synthetic-endor-key +export ENDOR_API_CREDENTIALS_SECRET=synthetic-endor-secret +export VIRUSTOTAL_API_KEY=synthetic-virustotal-key +export UNKNOWN_ADDITIONAL_SECRET=synthetic-unknown-secret +mkdir -p "$ENDOR_FIXTURE_OUTPUT" +scan_output=$(/bin/sh "$scan_script" "$work/source") +test "$scan_output" = '{"all_findings":[],"blocking_findings":[],"warning_findings":[]}' +printf '%s\n' fixture-ok diff --git a/scripts/security/endor/scan.sh b/scripts/security/endor/scan.sh new file mode 100755 index 0000000000..f0ccd6abe6 --- /dev/null +++ b/scripts/security/endor/scan.sh @@ -0,0 +1,104 @@ +#!/bin/sh +set -eu +# ClawScan mounts the submitted artifact read-only. All writes stay in the container. +if [ ! -d "$1" ]; then set -- "$(dirname "$1")"; fi +if [ ! -f "$1/package.json" ]; then + echo "Endor requires package.json at the target root" >&2 + exit 1 +fi +if [ -n "$(find "$1" -name .npmrc -print -quit)" ]; then + echo "Endor cannot safely scan targets containing .npmrc" >&2 + exit 1 +fi +export NPM_CONFIG_IGNORE_SCRIPTS=true +export YARN_IGNORE_PATH=1 +export YARN_IGNORE_SCRIPTS=true +resolver_bin=$(mktemp -d) +resolver_path=$PATH +if ! env_executable=$(command -v env); then + echo "Endor runtime is missing env" >&2 + exit 1 +fi +if ! npm_executable=$(command -v npm); then + echo "Endor runtime is missing npm" >&2 + exit 1 +fi +if ! yarn_executable=$(command -v yarn); then + echo "Endor runtime is missing Yarn Classic" >&2 + exit 1 +fi +case "$env_executable:$npm_executable:$yarn_executable" in + /*:/*:/*) ;; + *) + echo "Endor runtime tool paths must be absolute" >&2 + exit 1 + ;; +esac +resolver_home="$resolver_bin/home" +mkdir "$resolver_home" +write_resolver_shim() { + shim_name=$1 + shim_executable=$2 + cat > "$resolver_bin/$shim_name" <"$report" 2>"$diagnostic" || status=$? +# The custom-scanner protocol requires JSON, including for a successful empty scan. +# Endor's 128 means policy findings; analysis failures must still fail the scanner. +node - "$status" "$report" "$diagnostic" <<'JS' +const fs = require("node:fs"); +const [status, reportPath, diagnosticPath] = process.argv.slice(2); +const diagnostic = fs.readFileSync(diagnosticPath, "utf8"); +process.stderr.write(diagnostic); +function fail(message) { + console.error(message); + process.exit(1); +} +if (status !== "0" && status !== "128") fail(`Endor exited ${status}`); +if (diagnostic.includes("ERROR ") || /\b[1-9][0-9]* (scan failure|call graph error)\(s\)/.test(diagnostic)) { + fail("Endor reported one or more analysis errors"); +} +let raw = fs.readFileSync(reportPath, "utf8"); +if (raw === "" && status === "0") { + raw = '{"all_findings":[],"blocking_findings":[],"warning_findings":[]}'; +} +let report; +try { report = JSON.parse(raw); } catch { fail("Endor returned invalid findings JSON"); } +for (const name of ["all_findings", "blocking_findings", "warning_findings"]) { + if (!Array.isArray(report?.[name]) || report[name].some(value => value === null || typeof value !== "object" || Array.isArray(value))) { + fail("Endor returned invalid findings JSON"); + } +} +process.stdout.write(raw); +JS diff --git a/scripts/security/endor/scan.test.ts b/scripts/security/endor/scan.test.ts new file mode 100644 index 0000000000..36bd0581f1 --- /dev/null +++ b/scripts/security/endor/scan.test.ts @@ -0,0 +1,192 @@ +/* @vitest-environment node */ +import { execFileSync } from "node:child_process"; +import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { afterEach, describe, expect, it } from "vitest"; +import { CommandFailure, runWorkerCommand } from "../../lib/runWorkerCommand"; + +const scanScript = fileURLToPath(new URL("./scan.sh", import.meta.url)); +const directories: string[] = []; +const emptyReport = { all_findings: [], blocking_findings: [], warning_findings: [] }; + +async function fixture(body: string) { + const root = await mkdtemp(join(tmpdir(), "clawhub-endor-wrapper-")); + directories.push(root); + const source = join(root, "source"); + const bin = join(root, "bin"); + await mkdir(source); + await mkdir(bin); + await writeFile(join(source, "package.json"), '{"name":"wrapper-fixture"}\n'); + for (const [name, code] of Object.entries({ endorctl: body, npm: "", yarn: "" })) { + const path = join(bin, name); + await writeFile(path, `#!/usr/bin/env node\n${code}\n`); + await chmod(path, 0o755); + } + const env = { + PATH: `${bin}:${process.env.PATH}`, + HOME: root, + TMPDIR: root, + ENDOR_NAMESPACE: "fixture-namespace", + ENDOR_TOKEN: "fixture-token", + ENDOR_API_CREDENTIALS_KEY: "fixture-key", + ENDOR_API_CREDENTIALS_SECRET: "fixture-secret", + UNRELATED_SECRET: "fixture-unrelated", + }; + return { root, source, bin, env }; +} + +async function run(test: Awaited>, target = test.source) { + return runWorkerCommand("/bin/sh", [scanScript, target], { + commandLabel: "Endor wrapper", + cwd: test.root, + env: test.env, + timeoutMs: 10_000, + }); +} + +afterEach(async () => { + await Promise.all( + directories.splice(0).map((path) => rm(path, { recursive: true, force: true })), + ); +}); + +describe("Endor custom-scanner wrapper", () => { + it.each([0, 128])("preserves findings when Endor exits %s", async (status) => { + const report = { + all_findings: [ + { uuid: "reachable", spec: { finding_tags: ["FINDING_TAGS_REACHABLE_FUNCTION"] } }, + ], + blocking_findings: [{ uuid: "reachable" }], + warning_findings: [], + }; + const raw = JSON.stringify(report); + const test = await fixture( + `process.stdout.write(${JSON.stringify(raw)}); process.exit(${status});`, + ); + expect((await run(test)).stdout).toBe(raw); + }); + + it("normalizes only successful zero-byte output to an empty report", async () => { + const test = await fixture(""); + expect(JSON.parse((await run(test)).stdout)).toEqual(emptyReport); + }); + + it.each([ + { name: "whitespace", raw: " \n", status: 0, stderr: "" }, + { name: "malformed JSON", raw: "not-json", status: 0, stderr: "" }, + { name: "null report", raw: "null", status: 0, stderr: "" }, + { name: "missing findings", raw: "{}", status: 0, stderr: "" }, + { + name: "null finding", + raw: JSON.stringify({ ...emptyReport, all_findings: [null] }), + status: 0, + stderr: "", + }, + { + name: "non-object finding", + raw: JSON.stringify({ ...emptyReport, warning_findings: ["bad"] }), + status: 0, + stderr: "", + }, + { name: "empty policy failure", raw: "", status: 128, stderr: "" }, + { name: "command failure with JSON", raw: JSON.stringify(emptyReport), status: 7, stderr: "" }, + { + name: "error log despite zero exit", + raw: JSON.stringify(emptyReport), + status: 0, + stderr: "ERROR dependency scan failed", + }, + { + name: "scan failure summary", + raw: JSON.stringify(emptyReport), + status: 128, + stderr: "1 scan failure(s)", + }, + { + name: "call graph failure summary", + raw: JSON.stringify(emptyReport), + status: 0, + stderr: "2 call graph error(s)", + }, + ])("fails without usable JSON for $name", async ({ raw, status, stderr }) => { + const test = await fixture( + `process.stdout.write(${JSON.stringify(raw)}); process.stderr.write(${JSON.stringify(stderr)}); process.exit(${status});`, + ); + const failure = await run(test).catch((error: unknown) => error); + expect(failure).toBeInstanceOf(CommandFailure); + if (!(failure instanceof CommandFailure)) throw new Error("expected a command failure"); + expect(failure.stdout).toBe(""); + expect(failure.stderr).toContain("Endor"); + }); + + it.each([".npmrc", "nested/.npmrc"])("rejects %s before running Endor", async (name) => { + const test = await fixture('console.error("ENDOR_WAS_RUN");'); + await mkdir(join(test.source, "nested")); + await writeFile(join(test.source, name), "registry=https://example.invalid\n"); + const failure = await run(test).catch((error: unknown) => error); + if (!(failure instanceof CommandFailure)) throw new Error("expected a command failure"); + expect(failure.stderr).toContain(".npmrc"); + expect(failure.stderr).not.toContain("ENDOR_WAS_RUN"); + }); + + it("scans a fresh snapshot and keeps credentials away from package managers", async () => { + const test = await fixture(""); + const observation = join(test.root, "observation.json"); + await writeFile(join(test.source, "openclaw.plugin.json"), '{"id":"fixture"}'); + execFileSync("git", ["init", "-q", test.source]); + execFileSync("git", [ + "-C", + test.source, + "remote", + "add", + "origin", + "https://example.invalid/untrusted", + ]); + const sourceConfig = await readFile(join(test.source, ".git", "config"), "utf8"); + for (const name of ["npm", "yarn"]) { + await writeFile( + join(test.bin, name), + `#!/usr/bin/env node\nprocess.stdout.write(JSON.stringify(process.env));\n`, + ); + } + await writeFile( + join(test.bin, "endorctl"), + `#!/usr/bin/env node +const fs = require("node:fs"); +const { execFileSync } = require("node:child_process"); +fs.writeFileSync(${JSON.stringify(observation)}, JSON.stringify({ + cwd: process.cwd(), + token: process.env.ENDOR_TOKEN, + origin: execFileSync("git", ["remote", "get-url", "origin"], {encoding:"utf8"}).trim(), + npm: JSON.parse(execFileSync("npm", [], {encoding:"utf8"})), + yarn: JSON.parse(execFileSync("yarn", [], {encoding:"utf8"})), + args: process.argv.slice(2), +})); +fs.writeFileSync("generated-by-endor", "disposable"); +process.stdout.write(${JSON.stringify(JSON.stringify(emptyReport))}); +`, + ); + expect(JSON.parse((await run(test, join(test.source, "openclaw.plugin.json"))).stdout)).toEqual( + emptyReport, + ); + const observed = JSON.parse(await readFile(observation, "utf8")); + expect(observed.cwd).not.toBe(test.source); + expect(observed.token).toBe("fixture-token"); + expect(observed.origin).toBe("https://example.invalid/clawscan/scan-target.git"); + expect(observed.args).toContain("--dry-run"); + expect(observed.args).toContain("--build=false"); + for (const resolver of [observed.npm, observed.yarn]) { + expect(resolver.NPM_CONFIG_IGNORE_SCRIPTS).toBe("true"); + expect(resolver.YARN_IGNORE_PATH).toBe("1"); + expect(resolver.YARN_IGNORE_SCRIPTS).toBe("true"); + expect(JSON.stringify(resolver)).not.toContain("fixture-token"); + expect(JSON.stringify(resolver)).not.toContain("fixture-key"); + expect(JSON.stringify(resolver)).not.toContain("fixture-secret"); + expect(JSON.stringify(resolver)).not.toContain("fixture-unrelated"); + } + expect(await readFile(join(test.source, ".git", "config"), "utf8")).toBe(sourceConfig); + await expect(readFile(join(test.source, "generated-by-endor"))).rejects.toThrow(); + }); +}); diff --git a/scripts/security/run-codex-scan-worker-clawscan.test.ts b/scripts/security/run-codex-scan-worker-clawscan.test.ts index b16dbfdb08..7a7f085fdc 100644 --- a/scripts/security/run-codex-scan-worker-clawscan.test.ts +++ b/scripts/security/run-codex-scan-worker-clawscan.test.ts @@ -14,6 +14,10 @@ import { resolveBundledSkillSpectorScanInputs, runClawScan, } from "./run-codex-scan-worker"; +import { + calculateSecurityScanWorkerHealthSummary, + renderSecurityScanWorkerSummaryMarkdown, +} from "./security-scan-worker-summary"; const tempDirs: string[] = []; const execFileAsync = promisify(execFile); @@ -137,6 +141,27 @@ async function writeFakeClawScanCommand(path: string, body: string) { await chmod(path, 0o755); } +async function pathWithFakeDocker( + workspace: string, + currentPath: string | undefined, + body = `if [[ "$1" == "container" && "$2" == "ls" ]]; then exit 0; fi +echo "unexpected docker command: $*" >&2 +exit 99`, +) { + const binDirectory = join(workspace, "fake-docker-bin"); + const path = join(binDirectory, "docker"); + await mkdir(binDirectory, { recursive: true }); + await writeFile( + path, + `#!/usr/bin/env bash +set -euo pipefail +${body} +`, + ); + await chmod(path, 0o755); + return currentPath ? `${binDirectory}:${currentPath}` : binDirectory; +} + type ClawScanVerdict = "benign" | "suspicious" | "malicious"; function completeJudgeDimensions() { @@ -261,6 +286,31 @@ function clawScanArtifactJson(options?: { return JSON.stringify(artifact); } +function endorArtifactJson() { + return JSON.stringify({ + completedAt: "2026-09-15T00:00:00Z", + scanners: { + endor: { + status: "completed", + raw: { + all_findings: [ + { + uuid: "reachable-finding", + spec: { + extra_key: "GHSA-reachable", + finding_tags: ["FINDING_TAGS_REACHABLE_FUNCTION"], + level: "FINDING_LEVEL_HIGH", + }, + }, + ], + blocking_findings: [], + warning_findings: [], + }, + }, + }, + }); +} + describe("run-codex-scan-worker clawscan authority", () => { it("drops non-finite and invalid A.I.G SARIF line numbers", () => { const analysis = normalizeAigAnalysis( @@ -296,7 +346,7 @@ describe("run-codex-scan-worker clawscan authority", () => { const environmentLog = join(workspace, "clawscan-environment.log"); await writeFakeClawScanCommand( fakeClawScan, - `printf '%s\\n' "\${DEFAULT_BASE_URL-}" "\${OPENAI_BASE_URL-}" "\${OPENAI_API_KEY-}" "\${SECURITY_SCAN_WORKER_TOKEN-}" > ${JSON.stringify(environmentLog)} + `printf '%s\\n' "\${DEFAULT_BASE_URL-}" "\${OPENAI_BASE_URL-}" "\${OPENAI_API_KEY-}" "\${SECURITY_SCAN_WORKER_TOKEN-}" "\${ENDOR_TOKEN-}" "\${ENDOR_API_CREDENTIALS_KEY-}" "\${ENDOR_API_CREDENTIALS_SECRET-}" > ${JSON.stringify(environmentLog)} out="" while [[ $# -gt 0 ]]; do case "$1" in @@ -320,12 +370,18 @@ JSON`, const previousOpenAiBaseUrl = process.env.OPENAI_BASE_URL; const previousOpenAiApiKey = process.env.OPENAI_API_KEY; const previousWorkerToken = process.env.SECURITY_SCAN_WORKER_TOKEN; + const previousEndorToken = process.env.ENDOR_TOKEN; + const previousEndorKey = process.env.ENDOR_API_CREDENTIALS_KEY; + const previousEndorSecret = process.env.ENDOR_API_CREDENTIALS_SECRET; process.env.CODEX_SECURITY_SCAN_CLAWSCAN_COMMAND = fakeClawScan; process.env.CODEX_SECURITY_SCAN_CLAWSCAN_SANDBOX = "off"; process.env.DEFAULT_BASE_URL = "https://api.openai.com/v1"; process.env.OPENAI_BASE_URL = "https://unapproved.example.invalid/v1"; process.env.OPENAI_API_KEY = "mock-provider-key"; process.env.SECURITY_SCAN_WORKER_TOKEN = "mock-worker-token"; + process.env.ENDOR_TOKEN = "mock-endor-token"; + process.env.ENDOR_API_CREDENTIALS_KEY = "mock-endor-key"; + process.env.ENDOR_API_CREDENTIALS_SECRET = "mock-endor-secret"; try { const onDiagnostic = vi.fn(); @@ -347,6 +403,9 @@ JSON`, "mock-provider-key", "", "", + "", + "", + "", ]); } finally { if (previousCommand === undefined) delete process.env.CODEX_SECURITY_SCAN_CLAWSCAN_COMMAND; @@ -361,6 +420,12 @@ JSON`, else process.env.OPENAI_API_KEY = previousOpenAiApiKey; if (previousWorkerToken === undefined) delete process.env.SECURITY_SCAN_WORKER_TOKEN; else process.env.SECURITY_SCAN_WORKER_TOKEN = previousWorkerToken; + if (previousEndorToken === undefined) delete process.env.ENDOR_TOKEN; + else process.env.ENDOR_TOKEN = previousEndorToken; + if (previousEndorKey === undefined) delete process.env.ENDOR_API_CREDENTIALS_KEY; + else process.env.ENDOR_API_CREDENTIALS_KEY = previousEndorKey; + if (previousEndorSecret === undefined) delete process.env.ENDOR_API_CREDENTIALS_SECRET; + else process.env.ENDOR_API_CREDENTIALS_SECRET = previousEndorSecret; } }); @@ -964,6 +1029,506 @@ JSON`, }, ); + it("waits for Endor cleanup and completes with the primary malicious verdict", async () => { + const workspace = await tempDir(); + const diagnosticsRoot = await tempDir(); + const fakeClawScan = join(workspace, "fake-clawscan"); + const primaryCompleted = join(workspace, "primary-completed"); + const endorCleanupCompleted = join(workspace, "endor-cleanup-completed"); + await writeFakeClawScanCommand( + fakeClawScan, + `is_endor=false +output="" +while [[ $# -gt 0 ]]; do + case "$1" in + --profile) + if [[ "$2" == "endor" ]]; then is_endor=true; fi + shift 2 + ;; + --output) + output="$2" + shift 2 + ;; + *) + shift + ;; + esac +done +if [[ "$is_endor" == "true" ]]; then + cat > "$output" <<'JSON' +{"completedAt":"2026-09-16T00:00:00Z","scanners":{"endor":{"status":"failed","error":"Dependency resolution failed with ENDOR_TOKEN=fixture-token"}}} +JSON + exit 0 +fi +sleep 0.1 +cat > "$output" <<'JSON' +${clawScanArtifactJson({ verdict: "malicious" })} +JSON +touch ${JSON.stringify(primaryCompleted)}`, + ); + const packageJson = '{"name":"fixture-plugin","version":"1.0.0"}\n'; + const pluginManifest = '{"id":"fixture-plugin"}\n'; + const job = claimedJob({ + jobId: "securityScanJobs:endor-failed-summary", + source: "publish", + targetKind: "packageRelease", + target: { + files: [ + { + path: "package/package.json", + sha256: sha256(packageJson), + size: Buffer.byteLength(packageJson), + url: `data:application/json,${encodeURIComponent(packageJson)}`, + }, + { + path: "package/openclaw.plugin.json", + sha256: sha256(pluginManifest), + size: Buffer.byteLength(pluginManifest), + url: `data:application/json,${encodeURIComponent(pluginManifest)}`, + }, + ], + }, + }); + const previousEnv = { + command: process.env.CODEX_SECURITY_SCAN_CLAWSCAN_COMMAND, + enabled: process.env.CODEX_SECURITY_SCAN_ENDOR_ENABLED, + image: process.env.CODEX_SECURITY_SCAN_ENDOR_IMAGE, + namespace: process.env.ENDOR_NAMESPACE, + path: process.env.PATH, + token: process.env.ENDOR_TOKEN, + }; + process.env.CODEX_SECURITY_SCAN_CLAWSCAN_COMMAND = fakeClawScan; + process.env.CODEX_SECURITY_SCAN_ENDOR_ENABLED = "1"; + process.env.CODEX_SECURITY_SCAN_ENDOR_IMAGE = "clawscan-endor:test"; + process.env.ENDOR_NAMESPACE = "fixture-namespace"; + process.env.ENDOR_TOKEN = "fixture-token"; + process.env.PATH = await pathWithFakeDocker( + workspace, + previousEnv.path, + `if [[ "$1" == "container" && "$2" == "ls" ]]; then + touch ${JSON.stringify(endorCleanupCompleted)} + exit 0 +fi +echo "unexpected docker command: $*" >&2 +exit 99`, + ); + let primaryHadSettledAtCompletion = false; + let endorCleanupHadSettledAtCompletion = false; + + try { + const client = { + action: vi.fn(async (..._args: unknown[]) => { + primaryHadSettledAtCompletion = await readFile(primaryCompleted, "utf8") + .then(() => true) + .catch(() => false); + endorCleanupHadSettledAtCompletion = await readFile(endorCleanupCompleted, "utf8") + .then(() => true) + .catch(() => false); + return {}; + }), + }; + + await expect(processJob(client, "worker-auth", job, diagnosticsRoot)).resolves.toEqual({ + completed: true, + hardFailed: false, + retryableFailed: false, + }); + expect(primaryHadSettledAtCompletion).toBe(true); + expect(endorCleanupHadSettledAtCompletion).toBe(true); + expect(client.action).toHaveBeenCalledTimes(1); + expect(client.action.mock.calls[0]?.[1]).toMatchObject({ + endorAnalysis: { + status: "failed", + checkedAt: expect.any(Number), + reason: "Endor analysis failed. Request a rescan to try again.", + }, + llmAnalysis: { + status: "malicious", + verdict: "malicious", + }, + }); + expect(client.action.mock.calls[0]?.[1]).not.toHaveProperty("error"); + const jobDir = join(diagnosticsRoot, "securityScanJobs_endor-failed-summary"); + const artifact = await readFile( + join(jobDir, "endor-clawscan-artifact.redacted.json"), + "utf8", + ); + const diagnostic = JSON.parse(await readFile(join(jobDir, "diagnostic.json"), "utf8")); + expect(artifact).toContain("Dependency resolution failed"); + expect(artifact).not.toContain("fixture-token"); + expect(diagnostic.endorResult).toMatchObject({ + exitCode: 0, + rawArtifactPath: "endor-clawscan-artifact.redacted.json", + scannerError: "Dependency resolution failed with ENDOR_TOKEN=[redacted-secret]", + }); + expect(diagnostic.llmAnalysis).toMatchObject({ + status: "malicious", + verdict: "malicious", + }); + } finally { + if (previousEnv.command === undefined) + delete process.env.CODEX_SECURITY_SCAN_CLAWSCAN_COMMAND; + else process.env.CODEX_SECURITY_SCAN_CLAWSCAN_COMMAND = previousEnv.command; + if (previousEnv.enabled === undefined) delete process.env.CODEX_SECURITY_SCAN_ENDOR_ENABLED; + else process.env.CODEX_SECURITY_SCAN_ENDOR_ENABLED = previousEnv.enabled; + if (previousEnv.image === undefined) delete process.env.CODEX_SECURITY_SCAN_ENDOR_IMAGE; + else process.env.CODEX_SECURITY_SCAN_ENDOR_IMAGE = previousEnv.image; + if (previousEnv.namespace === undefined) delete process.env.ENDOR_NAMESPACE; + else process.env.ENDOR_NAMESPACE = previousEnv.namespace; + if (previousEnv.path === undefined) delete process.env.PATH; + else process.env.PATH = previousEnv.path; + if (previousEnv.token === undefined) delete process.env.ENDOR_TOKEN; + else process.env.ENDOR_TOKEN = previousEnv.token; + } + }); + + it("retains diagnostics when the primary and Endor commands both fail", async () => { + const workspace = await tempDir(); + const diagnosticsRoot = await tempDir(); + const fakeClawScan = join(workspace, "fake-clawscan"); + await writeFakeClawScanCommand( + fakeClawScan, + `is_endor=false +while [[ $# -gt 0 ]]; do + if [[ "$1" == "--profile" && "$2" == "endor" ]]; then is_endor=true; fi + shift +done +if [[ "$is_endor" == "true" ]]; then + echo "ENDOR_TOKEN=fixture-token Endor command failed" >&2 + exit 17 +fi +echo "primary ClawScan command failed" >&2 +exit 18`, + ); + const packageJson = '{"name":"fixture-plugin","version":"1.0.0"}\n'; + const job = claimedJob({ + jobId: "securityScanJobs:both-commands-failed", + source: "publish", + targetKind: "packageRelease", + target: { + files: [ + { + path: "package/package.json", + sha256: sha256(packageJson), + size: Buffer.byteLength(packageJson), + url: `data:application/json,${encodeURIComponent(packageJson)}`, + }, + ], + }, + }); + const previousEnv = { + command: process.env.CODEX_SECURITY_SCAN_CLAWSCAN_COMMAND, + enabled: process.env.CODEX_SECURITY_SCAN_ENDOR_ENABLED, + image: process.env.CODEX_SECURITY_SCAN_ENDOR_IMAGE, + namespace: process.env.ENDOR_NAMESPACE, + path: process.env.PATH, + token: process.env.ENDOR_TOKEN, + }; + process.env.CODEX_SECURITY_SCAN_CLAWSCAN_COMMAND = fakeClawScan; + process.env.CODEX_SECURITY_SCAN_ENDOR_ENABLED = "1"; + process.env.CODEX_SECURITY_SCAN_ENDOR_IMAGE = "clawscan-endor:test"; + process.env.ENDOR_NAMESPACE = "fixture-namespace"; + process.env.ENDOR_TOKEN = "fixture-token"; + process.env.PATH = await pathWithFakeDocker(workspace, previousEnv.path); + + try { + const client = { + action: vi.fn(async (...args: unknown[]) => { + const payload = args[1] as { error?: string } | undefined; + return payload?.error ? { retry: true } : {}; + }), + }; + await expect(processJob(client, "worker-auth", job, diagnosticsRoot)).resolves.toEqual({ + completed: false, + hardFailed: false, + retryableFailed: true, + }); + + const jobDir = join(diagnosticsRoot, "securityScanJobs_both-commands-failed"); + const diagnostic = JSON.parse(await readFile(join(jobDir, "diagnostic.json"), "utf8")); + expect(diagnostic.clawscanResult).toMatchObject({ exitCode: 18 }); + expect(diagnostic.endorResult).toMatchObject({ exitCode: 17, timedOut: false }); + expect(await readFile(join(jobDir, "clawscan.stderr.redacted.log"), "utf8")).toContain( + "primary ClawScan command failed", + ); + const endorStderr = await readFile( + join(jobDir, "endor-clawscan.stderr.redacted.log"), + "utf8", + ); + expect(endorStderr).toContain("ENDOR_TOKEN=[redacted-secret]"); + expect(endorStderr).not.toContain("fixture-token"); + } finally { + if (previousEnv.command === undefined) + delete process.env.CODEX_SECURITY_SCAN_CLAWSCAN_COMMAND; + else process.env.CODEX_SECURITY_SCAN_CLAWSCAN_COMMAND = previousEnv.command; + if (previousEnv.enabled === undefined) delete process.env.CODEX_SECURITY_SCAN_ENDOR_ENABLED; + else process.env.CODEX_SECURITY_SCAN_ENDOR_ENABLED = previousEnv.enabled; + if (previousEnv.image === undefined) delete process.env.CODEX_SECURITY_SCAN_ENDOR_IMAGE; + else process.env.CODEX_SECURITY_SCAN_ENDOR_IMAGE = previousEnv.image; + if (previousEnv.namespace === undefined) delete process.env.ENDOR_NAMESPACE; + else process.env.ENDOR_NAMESPACE = previousEnv.namespace; + if (previousEnv.path === undefined) delete process.env.PATH; + else process.env.PATH = previousEnv.path; + if (previousEnv.token === undefined) delete process.env.ENDOR_TOKEN; + else process.env.ENDOR_TOKEN = previousEnv.token; + } + }); + + it("completes the primary job while reporting an Endor timeout to worker health", async () => { + const workspace = await tempDir(); + const fakeClawScan = join(workspace, "fake-clawscan"); + const dockerRunId = join(workspace, "docker-run-id"); + const dockerRemoved = join(workspace, "docker-removed"); + await writeFakeClawScanCommand( + fakeClawScan, + `is_endor=false +output="" +while [[ $# -gt 0 ]]; do + case "$1" in + --profile) + if [[ "$2" == "endor" ]]; then is_endor=true; fi + shift 2 + ;; + --output) + output="$2" + shift 2 + ;; + *) + shift + ;; + esac +done +if [[ "$is_endor" == "true" ]]; then + sleep 2 + exit 0 +fi +cat > "$output" <<'JSON' +${clawScanArtifactJson({ verdict: "benign" })} +JSON`, + ); + const packageJson = '{"name":"fixture-plugin","version":"1.0.0"}\n'; + const job = claimedJob({ + jobId: "securityScanJobs:endor-timeout-health", + source: "publish", + targetKind: "packageRelease", + target: { + files: [ + { + path: "package/package.json", + sha256: sha256(packageJson), + size: Buffer.byteLength(packageJson), + url: `data:application/json,${encodeURIComponent(packageJson)}`, + }, + ], + }, + }); + const previousEnv = { + command: process.env.CODEX_SECURITY_SCAN_CLAWSCAN_COMMAND, + enabled: process.env.CODEX_SECURITY_SCAN_ENDOR_ENABLED, + image: process.env.CODEX_SECURITY_SCAN_ENDOR_IMAGE, + namespace: process.env.ENDOR_NAMESPACE, + path: process.env.PATH, + timeout: process.env.CODEX_SECURITY_SCAN_ENDOR_TIMEOUT_MS, + token: process.env.ENDOR_TOKEN, + }; + process.env.CODEX_SECURITY_SCAN_CLAWSCAN_COMMAND = fakeClawScan; + process.env.CODEX_SECURITY_SCAN_ENDOR_ENABLED = "1"; + process.env.CODEX_SECURITY_SCAN_ENDOR_IMAGE = "clawscan-endor:test"; + process.env.CODEX_SECURITY_SCAN_ENDOR_TIMEOUT_MS = "25"; + process.env.ENDOR_NAMESPACE = "fixture-namespace"; + process.env.ENDOR_TOKEN = "fixture-token"; + process.env.PATH = await pathWithFakeDocker( + workspace, + previousEnv.path, + `case "$2" in + ls) + printf '%s' "\${6##*=}" > ${JSON.stringify(dockerRunId)} + if [[ ! -f ${JSON.stringify(dockerRemoved)} ]]; then printf '%s\\n' 0123456789ab; fi + ;; + inspect) + printf '%s\\n%s\\n' "$(cat ${JSON.stringify(dockerRunId)})" command123 + ;; + rm) + [[ "$5" == "0123456789ab" ]] + touch ${JSON.stringify(dockerRemoved)} + ;; + *) exit 99 ;; +esac`, + ); + + try { + const client = { + action: vi.fn(async (...args: unknown[]) => { + const payload = args[1] as { error?: string } | undefined; + return payload?.error ? { retry: true } : {}; + }), + }; + const onHealth = vi.fn(); + + await expect(processJob(client, "worker-auth", job, undefined, onHealth)).resolves.toEqual({ + completed: true, + hardFailed: false, + retryableFailed: false, + }); + expect(client.action.mock.calls[0]?.[1]).toMatchObject({ + endorAnalysis: { + status: "failed", + checkedAt: expect.any(Number), + reason: "Endor analysis failed. Request a rescan to try again.", + }, + llmAnalysis: { status: "clean", verdict: "benign" }, + }); + expect(client.action.mock.calls[0]?.[1]).not.toHaveProperty("error"); + await expect(readFile(dockerRemoved, "utf8")).resolves.toBe(""); + expect(onHealth).toHaveBeenCalledWith( + expect.objectContaining({ + completed: true, + failureStage: undefined, + judgeStageFailed: false, + scannerStageFailed: true, + timedOut: true, + }), + ); + const summary = calculateSecurityScanWorkerHealthSummary({ + durationMs: 1_000, + outcomes: [onHealth.mock.calls[0]![0]], + pool: { + totalClaimed: 1, + totalClaimFailures: 0, + totalCompleted: 1, + totalFailed: 0, + totalRetryableFailed: 0, + }, + workerId: "fixture-worker", + }); + expect(renderSecurityScanWorkerSummaryMarkdown(summary)).toContain("| Timed out | 1 |"); + } finally { + if (previousEnv.command === undefined) + delete process.env.CODEX_SECURITY_SCAN_CLAWSCAN_COMMAND; + else process.env.CODEX_SECURITY_SCAN_CLAWSCAN_COMMAND = previousEnv.command; + if (previousEnv.enabled === undefined) delete process.env.CODEX_SECURITY_SCAN_ENDOR_ENABLED; + else process.env.CODEX_SECURITY_SCAN_ENDOR_ENABLED = previousEnv.enabled; + if (previousEnv.image === undefined) delete process.env.CODEX_SECURITY_SCAN_ENDOR_IMAGE; + else process.env.CODEX_SECURITY_SCAN_ENDOR_IMAGE = previousEnv.image; + if (previousEnv.namespace === undefined) delete process.env.ENDOR_NAMESPACE; + else process.env.ENDOR_NAMESPACE = previousEnv.namespace; + if (previousEnv.path === undefined) delete process.env.PATH; + else process.env.PATH = previousEnv.path; + if (previousEnv.timeout === undefined) + delete process.env.CODEX_SECURITY_SCAN_ENDOR_TIMEOUT_MS; + else process.env.CODEX_SECURITY_SCAN_ENDOR_TIMEOUT_MS = previousEnv.timeout; + if (previousEnv.token === undefined) delete process.env.ENDOR_TOKEN; + else process.env.ENDOR_TOKEN = previousEnv.token; + } + }); + + it("persists the Endor summary without uploading any package scanner report", async () => { + const workspace = await tempDir(); + const fakeClawScan = join(workspace, "fake-clawscan"); + await writeFakeClawScanCommand( + fakeClawScan, + `is_endor=false +output="" +while [[ $# -gt 0 ]]; do + case "$1" in + --profile) + if [[ "$2" == "endor" ]]; then is_endor=true; fi + shift 2 + ;; + --output) + output="$2" + shift 2 + ;; + *) + shift + ;; + esac +done +if [[ "$is_endor" == "true" ]]; then + cat > "$output" <<'JSON' +${endorArtifactJson()} +JSON +else + cat > "$output" <<'JSON' +${clawScanArtifactJson({ verdict: "benign" })} +JSON +fi`, + ); + const packageJson = '{"name":"fixture-plugin","version":"1.0.0"}\n'; + const job: ClaimedJob = { + ...claimedJob({ + jobId: "securityScanJobs:endor-complete", + source: "publish", + targetKind: "packageRelease", + target: { + files: [ + { + path: "package/package.json", + sha256: sha256(packageJson), + size: Buffer.byteLength(packageJson), + url: `data:application/json,${encodeURIComponent(packageJson)}`, + }, + ], + }, + }), + scannerReportsUploadUrl: "https://storage.example/endor-report-upload", + }; + const previousEnv = { + command: process.env.CODEX_SECURITY_SCAN_CLAWSCAN_COMMAND, + enabled: process.env.CODEX_SECURITY_SCAN_ENDOR_ENABLED, + image: process.env.CODEX_SECURITY_SCAN_ENDOR_IMAGE, + namespace: process.env.ENDOR_NAMESPACE, + path: process.env.PATH, + token: process.env.ENDOR_TOKEN, + }; + process.env.CODEX_SECURITY_SCAN_CLAWSCAN_COMMAND = fakeClawScan; + process.env.CODEX_SECURITY_SCAN_ENDOR_ENABLED = "1"; + process.env.CODEX_SECURITY_SCAN_ENDOR_IMAGE = "clawscan-endor:test"; + process.env.ENDOR_NAMESPACE = "fixture-namespace"; + process.env.ENDOR_TOKEN = "fixture-token"; + process.env.PATH = await pathWithFakeDocker(workspace, previousEnv.path); + const fetchOriginal = globalThis.fetch; + let uploadedReport: unknown; + vi.spyOn(globalThis, "fetch").mockImplementation(async (url, init) => { + if (url !== job.scannerReportsUploadUrl) return fetchOriginal(url, init); + uploadedReport = JSON.parse(String(init?.body)); + return new Response(JSON.stringify({ storageId: "storage:endor-report" })); + }); + + try { + const client = { action: vi.fn(async (..._args: unknown[]) => ({})) }; + await expect(processJob(client, "worker-auth", job, undefined)).resolves.toEqual({ + completed: true, + hardFailed: false, + retryableFailed: false, + }); + expect(client.action).toHaveBeenCalledTimes(1); + expect(client.action.mock.calls[0]?.[1]).toMatchObject({ + endorAnalysis: { + status: "completed", + reachableFunctionCount: 1, + findings: [{ severity: "high", summary: "GHSA-reachable" }], + }, + }); + expect(client.action.mock.calls[0]?.[1]).not.toHaveProperty("scannerReportsStorageId"); + expect(uploadedReport).toBeUndefined(); + } finally { + if (previousEnv.command === undefined) + delete process.env.CODEX_SECURITY_SCAN_CLAWSCAN_COMMAND; + else process.env.CODEX_SECURITY_SCAN_CLAWSCAN_COMMAND = previousEnv.command; + if (previousEnv.enabled === undefined) delete process.env.CODEX_SECURITY_SCAN_ENDOR_ENABLED; + else process.env.CODEX_SECURITY_SCAN_ENDOR_ENABLED = previousEnv.enabled; + if (previousEnv.image === undefined) delete process.env.CODEX_SECURITY_SCAN_ENDOR_IMAGE; + else process.env.CODEX_SECURITY_SCAN_ENDOR_IMAGE = previousEnv.image; + if (previousEnv.namespace === undefined) delete process.env.ENDOR_NAMESPACE; + else process.env.ENDOR_NAMESPACE = previousEnv.namespace; + if (previousEnv.path === undefined) delete process.env.PATH; + else process.env.PATH = previousEnv.path; + if (previousEnv.token === undefined) delete process.env.ENDOR_TOKEN; + else process.env.ENDOR_TOKEN = previousEnv.token; + } + }); + it("fails the job when SkillSpector scanner status is skipped", async () => { const workspace = await tempDir(); const fakeClawScan = join(workspace, "fake-clawscan"); diff --git a/scripts/security/run-codex-scan-worker.test.ts b/scripts/security/run-codex-scan-worker.test.ts index f6eb249245..6bbf32c4c9 100644 --- a/scripts/security/run-codex-scan-worker.test.ts +++ b/scripts/security/run-codex-scan-worker.test.ts @@ -1278,4 +1278,61 @@ describe("run-codex-scan-worker diagnostics", () => { }, ]); }); + + it("writes bounded Endor failure artifacts and command diagnostics with secrets redacted", async () => { + const diagnosticsRoot = await tempDir(); + const longDetail = "x".repeat(21_000); + await writeJobDiagnostic({ + completedAt: 2000, + diagnosticsRoot, + endor: { + args: ["clawscan", "./endor-artifact/openclaw.plugin.json", "--scanner", "endor"], + artifactPath: "/tmp/removed-endor-artifact.json", + exitCode: 1, + rawArtifact: JSON.stringify({ + scanners: { + endor: { + status: "failed", + error: `Dependency resolution failed with ENDOR_TOKEN=fixture-token ${longDetail}`, + }, + }, + }), + scannerError: "Dependency resolution failed with ENDOR_TOKEN=fixture-token", + stderr: "ENDOR_TOKEN=fixture-token\ncommand failed\n", + stdout: "scan started\n", + timedOut: true, + }, + job: { + job: { + _id: "job-endor-failure-evidence", + hasMaliciousSignal: false, + leaseToken: "placeholder", + source: "publish", + targetKind: "packageRelease", + waitForVtUntil: 0, + }, + target: {}, + }, + startedAt: 1000, + status: "failed", + }); + + const jobDir = join(diagnosticsRoot, "job-endor-failure-evidence"); + const artifact = await readFile(join(jobDir, "endor-clawscan-artifact.redacted.json"), "utf8"); + const stderr = await readFile(join(jobDir, "endor-clawscan.stderr.redacted.log"), "utf8"); + const diagnostic = JSON.parse(await readFile(join(jobDir, "diagnostic.json"), "utf8")); + expect(artifact).toContain("Dependency resolution failed"); + expect(artifact).toContain("...[truncated "); + expect(artifact).not.toContain("fixture-token"); + expect(stderr).toContain("ENDOR_TOKEN=[redacted-secret]"); + expect(stderr).not.toContain("fixture-token"); + expect(diagnostic.endorResult).toMatchObject({ + exitCode: 1, + rawArtifactPath: "endor-clawscan-artifact.redacted.json", + scannerError: "Dependency resolution failed with ENDOR_TOKEN=[redacted-secret]", + stderrPath: "endor-clawscan.stderr.redacted.log", + stdoutPath: "endor-clawscan.stdout.redacted.log", + timedOut: true, + }); + }); }); diff --git a/scripts/security/run-codex-scan-worker.ts b/scripts/security/run-codex-scan-worker.ts index 509265d80b..b3c59280ae 100644 --- a/scripts/security/run-codex-scan-worker.ts +++ b/scripts/security/run-codex-scan-worker.ts @@ -1,16 +1,17 @@ -import { spawn } from "node:child_process"; import { mkdirSync, readFileSync } from "node:fs"; -import { appendFile, lstat, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { appendFile, mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { basename, dirname, extname, join, resolve, sep } from "node:path"; import { pathToFileURL } from "node:url"; import { ConvexHttpClient } from "convex/browser"; import { api } from "../../convex/_generated/api"; import type { Id } from "../../convex/_generated/dataModel"; +import type { EndorAnalysis } from "../../convex/lib/endorAnalysis"; import { parseLlmEvalResponse, type LlmEvalDimension } from "../../convex/lib/securityPrompt"; import { readWorkerAssignment } from "../../packages/clawhub-admin/src/scanAssignments"; import { assertCodexWorkerExecutionAllowed, resolveCodexWorkerHome } from "../codex-worker-guard"; import { materializeVerifiedArtifactFiles } from "../lib/artifactMaterialization"; +import { CommandFailure, runWorkerCommand } from "../lib/runWorkerCommand"; import { createWorkerLogger } from "../lib/workerLogger"; import { maskGitHubActionsSecret, @@ -19,6 +20,12 @@ import { redactWorkerPublicText, safeWorkerArtifactPathLabel, } from "../lib/workerRedaction"; +import { resolveClawScanTargetForRoot } from "./clawScanTarget"; +import { + isEndorPluginScanEnabled, + runEndorPluginScan, + type EndorCommandDiagnostic, +} from "./run-endor-plugin-scan"; import { calculateSecurityScanWorkerHealthSummary, renderSecurityScanWorkerSummaryMarkdown, @@ -165,6 +172,7 @@ type JobDiagnosticInput = { clawscan?: ClawScanCommandDiagnostic; completedAt: number; diagnosticsRoot?: string; + endor?: EndorCommandDiagnostic; error?: string; job: ClaimedJob; llmAnalysis?: unknown; @@ -216,6 +224,7 @@ const MAX_STORED_SKILLSPECTOR_SHORT_TEXT_CHARS = 512; const MAX_STORED_AIG_FINDINGS = 25; const AIG_UNSAFE_BYTECODE_EXTENSIONS = new Set([".pyc", ".pyd", ".pyo"]); const DEFAULT_LEASE_MS = 60 * 60 * 1000; +const ENDOR_SCAN_FAILED_REASON = "Endor analysis failed. Request a rescan to try again."; const logger = createWorkerLogger({ name: "security-scan-worker" }); const root = resolve(new URL("../..", import.meta.url).pathname); @@ -735,6 +744,27 @@ export async function writeJobDiagnostic(input: JobDiagnosticInput) { rawArtifact: input.clawscan.rawArtifact, }) : []; + const endorStdoutPath = await writeDiagnosticText( + jobDir, + "endor-clawscan.stdout.redacted.log", + input.endor?.stdout, + "endorClawscanStdout", + ); + const endorStderrPath = await writeDiagnosticText( + jobDir, + "endor-clawscan.stderr.redacted.log", + input.endor?.stderr, + "endorClawscanStderr", + ); + const endorArtifactPath = await writeDiagnosticText( + jobDir, + "endor-clawscan-artifact.redacted.json", + input.endor?.rawArtifact + ? redactEvidenceText(input.endor.rawArtifact, ["endorClawscanArtifact"]) + : undefined, + "endorClawscanArtifact", + { structured: false }, + ); const diagnostic = { completedAt: input.completedAt, @@ -776,6 +806,19 @@ export async function writeJobDiagnostic(input: JobDiagnosticInput) { stderrPath: clawscanStderrPath, stdoutPath: clawscanStdoutPath, }, + endorResult: input.endor + ? { + args: input.endor.args, + exitCode: input.endor.exitCode, + rawArtifactPath: endorArtifactPath, + scannerError: input.endor.scannerError + ? redactDiagnosticText(input.endor.scannerError) + : undefined, + stderrPath: endorStderrPath, + stdoutPath: endorStdoutPath, + timedOut: input.endor.timedOut, + } + : undefined, }; await writeFile(join(jobDir, "diagnostic.json"), `${JSON.stringify(diagnostic, null, 2)}\n`); @@ -884,94 +927,18 @@ function codexEnv(workspace: string) { return env; } -class CommandFailure extends Error { - exitCode: number | null; - stderr: string; - stdout: string; - timedOut: boolean; - - constructor( - message: string, - exitCode: number | null, - stdout: string, - stderr: string, - timedOut: boolean, - ) { - super(message); - this.name = "CommandFailure"; - this.exitCode = exitCode; - this.stdout = stdout; - this.stderr = stderr; - this.timedOut = timedOut; - } -} - async function runCommand( command: string, args: string[], options: { cwd: string; input?: string; omitEnv?: string[]; timeoutMs: number }, ) { - return await new Promise<{ stdout: string; stderr: string }>((resolvePromise, reject) => { - const env = codexEnv(options.cwd); - for (const name of options.omitEnv ?? []) delete env[name]; - const child = spawn(command, args, { - cwd: options.cwd, - detached: process.platform !== "win32", - env, - stdio: ["pipe", "pipe", "pipe"], - }); - let stdout = ""; - let stderr = ""; - let timedOut = false; - let forceKillTimeout: NodeJS.Timeout | undefined; - const killProcessTree = (signal: NodeJS.Signals) => { - if (process.platform !== "win32" && child.pid) { - try { - process.kill(-child.pid, signal); - return; - } catch { - // The process group may already have exited; fall back to the direct child. - } - } - child.kill(signal); - }; - const timeout = setTimeout(() => { - timedOut = true; - killProcessTree("SIGTERM"); - forceKillTimeout = setTimeout(() => killProcessTree("SIGKILL"), 10_000); - forceKillTimeout.unref(); - }, options.timeoutMs); - child.stdout.on("data", (chunk) => { - stdout += String(chunk); - }); - child.stderr.on("data", (chunk) => { - stderr += String(chunk); - }); - child.on("error", (error) => { - clearTimeout(timeout); - if (timedOut) killProcessTree("SIGKILL"); - if (forceKillTimeout) clearTimeout(forceKillTimeout); - reject(error); - }); - child.on("close", (code) => { - clearTimeout(timeout); - if (timedOut) killProcessTree("SIGKILL"); - if (forceKillTimeout) clearTimeout(forceKillTimeout); - if (code === 0) resolvePromise({ stdout, stderr }); - else { - reject( - new CommandFailure( - `${command} ${timedOut ? "timed out" : `exited ${code}`}; see redacted stdout/stderr diagnostics`, - code, - stdout, - stderr, - timedOut, - ), - ); - } - }); - if (options.input) child.stdin.end(options.input); - else child.stdin.end(); + const env = codexEnv(options.cwd); + for (const name of options.omitEnv ?? []) delete env[name]; + return await runWorkerCommand(command, args, { + cwd: options.cwd, + env, + ...(options.input !== undefined ? { input: options.input } : {}), + timeoutMs: options.timeoutMs, }); } @@ -1996,7 +1963,12 @@ export async function runClawScan( try { const output = await runCommand(command, args, { cwd: workspace, - omitEnv: ["VIRUSTOTAL_API_KEY"], + omitEnv: [ + "VIRUSTOTAL_API_KEY", + "ENDOR_TOKEN", + "ENDOR_API_CREDENTIALS_KEY", + "ENDOR_API_CREDENTIALS_SECRET", + ], timeoutMs: clawScanTimeoutMs(), }); onDiagnostic({ @@ -2035,34 +2007,36 @@ async function resolveScanArtifactRoot(workspace: string, job: ClaimedJob) { export async function resolveClawScanTarget(workspace: string, job: ClaimedJob) { const root = await resolveScanArtifactRoot(workspace, job); - const manifests = ["SKILL.md", "openclaw.plugin.json"]; - const present = await Promise.all( - manifests.map(async (name) => - (await lstat(join(workspace, root, name)).catch(() => null))?.isFile(), - ), - ); - // ClawScan rejects dual-manifest directories. An explicit manifest selects the - // claimed kind while ClawScan still scans the entire dual-layout directory. - return present.every(Boolean) - ? `${root}/${manifests[job.job.targetKind === "packageRelease" ? 1 : 0]}` - : root; + return await resolveClawScanTargetForRoot({ + artifactKind: job.job.targetKind === "packageRelease" ? "packageRelease" : "skill", + root, + workspace, + }); } export function scanHealthClassification(input: { clawscan: ClawScanCommandDiagnostic; + endor?: EndorCommandDiagnostic; errorMessage?: string; status: "completed" | "failed"; }) { - const timedOut = Boolean(input.clawscan.timedOut); + const timedOut = Boolean(input.clawscan.timedOut || input.endor?.timedOut); const scannerStatuses = Object.values(input.clawscan.mapping?.scanners ?? {}).filter( (status): status is string => Boolean(status), ); let scannerStageFailed = scannerStatuses.some( (status) => status !== "completed" && status !== "missing", ); + scannerStageFailed ||= Boolean( + input.endor?.timedOut || + input.endor?.scannerError || + (input.endor?.exitCode !== undefined && + input.endor.exitCode !== null && + input.endor.exitCode !== 0), + ); const judgeStatus = input.clawscan.mapping?.judge?.status; let judgeStageFailed = Boolean(judgeStatus && judgeStatus !== "completed"); - scannerStageFailed ||= /ClawScan scanner/i.test(input.errorMessage ?? ""); + scannerStageFailed ||= /ClawScan scanner|Endor ClawScan/i.test(input.errorMessage ?? ""); judgeStageFailed ||= /ClawScan (artifact )?judge|output schema/i.test(input.errorMessage ?? ""); const failureStage = @@ -2091,25 +2065,55 @@ export async function processJob( const workspace = await mkdtemp(join(tmpdir(), `clawhub-codex-scan-${basename(job.job._id)}-`)); const startedAt = Date.now(); const clawscan: ClawScanCommandDiagnostic = {}; + const endor: EndorCommandDiagnostic = {}; let errorMessage: string | undefined; let scanCompletedAt: number | undefined; let llmAnalysis: StoredLlmAnalysis | undefined; let aigAnalysis: AigAnalysis | undefined; let skillSpectorAnalysis: SkillSpectorAnalysis | undefined; + let endorAnalysis: EndorAnalysis | undefined; let status: JobDiagnosticInput["status"] = "failed"; try { await writeArtifactWorkspace(job, workspace); - const mapped = await runClawScan(job, workspace, (next) => { - Object.assign(clawscan, next); - }); + const shouldRunEndor = job.job.targetKind === "packageRelease" && isEndorPluginScanEnabled(); + const [clawScanResult, endorScanResult] = await Promise.allSettled([ + runClawScan(job, workspace, (next) => { + Object.assign(clawscan, next); + }), + shouldRunEndor + ? runEndorPluginScan({ + workspace, + onDiagnostic: (next) => { + Object.assign(endor, next); + }, + }) + : Promise.resolve(undefined), + ] as const); + if (clawScanResult.status === "rejected") throw clawScanResult.reason; + const mapped = clawScanResult.value; llmAnalysis = mapped.llmAnalysis; aigAnalysis = mapped.aigAnalysis; skillSpectorAnalysis = mapped.skillSpectorAnalysis; if (!llmAnalysis) throw new Error("Security scan did not produce llmAnalysis"); - // A signed upload URL advertises support after the separately deployed - // backend updates. Upload directly to storage to avoid action argument limits. + if (endorScanResult.status === "rejected") { + const endorError = sanitizeWorkerErrorMessage( + endorScanResult.reason instanceof Error + ? endorScanResult.reason.message + : String(endorScanResult.reason), + ); + endor.scannerError ??= endorError; + endorAnalysis = { + status: "failed", + checkedAt: Date.now(), + reason: ENDOR_SCAN_FAILED_REASON, + }; + } else { + endorAnalysis = endorScanResult.value; + } + // Package jobs persist scanner summaries only. Skill jobs retain their + // existing raw scanner upload when the backend advertises support. let scannerReportsStorageId: string | undefined; - if (job.scannerReportsUploadUrl) { + if (job.job.targetKind !== "packageRelease" && job.scannerReportsUploadUrl) { const uploaded = await fetch(job.scannerReportsUploadUrl, { method: "POST", headers: { "Content-Type": "application/json" }, @@ -2129,6 +2133,7 @@ export async function processJob( llmAnalysis, aigAnalysis, skillSpectorAnalysis, + ...(endorAnalysis ? { endorAnalysis } : {}), ...(scannerReportsStorageId ? { scannerReportsStorageId: scannerReportsStorageId as Id<"_storage"> } : {}), @@ -2150,6 +2155,7 @@ export async function processJob( ); const health = scanHealthClassification({ clawscan, + endor, status: "completed", }); onHealth?.({ @@ -2184,6 +2190,7 @@ export async function processJob( const completedAt = Date.now(); const health = scanHealthClassification({ clawscan, + endor, errorMessage, status: "failed", }); @@ -2203,6 +2210,7 @@ export async function processJob( completedAt: Date.now(), clawscan, diagnosticsRoot, + endor: Object.keys(endor).length > 0 ? endor : undefined, error: errorMessage, job, llmAnalysis, diff --git a/scripts/security/run-endor-plugin-scan.test.ts b/scripts/security/run-endor-plugin-scan.test.ts new file mode 100644 index 0000000000..756e3af3aa --- /dev/null +++ b/scripts/security/run-endor-plugin-scan.test.ts @@ -0,0 +1,544 @@ +/* @vitest-environment node */ +import { chmod, mkdir, mkdtemp, readFile, rm, symlink, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { afterEach, describe, expect, it } from "vitest"; +import { runEndorPluginScan, type EndorCommandDiagnostic } from "./run-endor-plugin-scan"; + +const tempDirs: string[] = []; + +afterEach(async () => { + await Promise.all(tempDirs.splice(0).map((dir) => rm(dir, { force: true, recursive: true }))); +}); + +async function tempDir() { + const directory = await mkdtemp(join(tmpdir(), "clawhub-endor-plugin-test-")); + tempDirs.push(directory); + await writeFakeDocker(directory); + return directory; +} + +async function writeFakeClawScan(path: string, body: string) { + await writeFile(path, `#!/usr/bin/env bash\nset -euo pipefail\n${body}\n`); + await chmod(path, 0o755); +} + +async function writeFakeDocker( + workspace: string, + body = `if [[ "$1" == "container" && "$2" == "ls" ]]; then exit 0; fi +echo "unexpected docker command: $*" >&2 +exit 99`, +) { + const binDirectory = join(workspace, "bin"); + const path = join(binDirectory, "docker"); + await mkdir(binDirectory, { recursive: true }); + await writeFile(path, `#!/usr/bin/env bash\nset -euo pipefail\n${body}\n`); + await chmod(path, 0o755); + return path; +} + +function testPath(workspace: string) { + return `${join(workspace, "bin")}:${process.env.PATH ?? ""}`; +} + +describe("runEndorPluginScan", () => { + it.each(["plugin", "claw"] as const)( + "scans a normalized disposable %s package and retains only exact reachable functions", + async (family) => { + const workspace = await tempDir(); + const packageRoot = join(workspace, "artifact", "package"); + await mkdir(packageRoot, { recursive: true }); + await writeFile( + join(packageRoot, "package.json"), + `${JSON.stringify({ + name: "fixture-plugin", + ...(family === "claw" ? { openclaw: { claw: "CLAW.md" } } : {}), + dependencies: { "runtime-workspace": "workspace:*", lodash: "4.17.20" }, + devDependencies: { "dev-workspace": "workspace:^", typescript: "6.0.3" }, + })}\n`, + ); + await writeFile(join(packageRoot, "npm-shrinkwrap.json"), '{"lockfileVersion":3}\n'); + if (family === "plugin") { + await writeFile(join(packageRoot, "SKILL.md"), "# Bundled skill\n"); + await writeFile(join(packageRoot, "openclaw.plugin.json"), '{"id":"fixture-plugin"}\n'); + } else { + await writeFile( + join(packageRoot, "CLAW.md"), + "---\nschemaVersion: 1\nagent:\n id: fixture-plugin\n---\n# Fixture Claw\n", + ); + } + + const command = join(workspace, "fake-clawscan"); + const argsLog = join(workspace, "args.log"); + const envLog = join(workspace, "env.log"); + await writeFakeClawScan( + command, + `printf '%s\\n' "$@" > ${JSON.stringify(argsLog)} +printf '%s\\n' "\${ENDOR_NAMESPACE-}" "\${ENDOR_TOKEN-}" "\${OPENAI_API_KEY-}" "\${SECURITY_SCAN_WORKER_TOKEN-}" "\${HOME-}" "\${DOCKER_CONFIG-}" "\${DOCKER_HOST-}" "\${CLAWSCAN_SANDBOX_RUN_ID-}" > ${JSON.stringify(envLog)} +output="" +while [[ $# -gt 0 ]]; do + if [[ "$1" == "--output" ]]; then output="$2"; shift 2; else shift; fi +done +cat > "$output" <<'JSON' +{ + "completedAt": "2026-09-15T00:00:00Z", + "scanners": { + "endor": { + "status": "completed", + "raw": { + "all_findings": [ + { + "uuid": "exact", + "meta": {"name": "exact finding"}, + "spec": { + "extra_key": "GHSA-exact", + "finding_tags": ["FINDING_TAGS_REACHABLE_FUNCTION"], + "level": "FINDING_LEVEL_HIGH", + "summary": "Reachable lodash function is vulnerable", + "target_dependency_package_name": "npm://lodash@4.17.20", + "target_dependency_version": "4.17.20" + } + }, + { + "uuid": "potential", + "spec": { + "extra_key": "GHSA-potential", + "finding_tags": ["FINDING_TAGS_POTENTIALLY_REACHABLE_FUNCTION"], + "level": "FINDING_LEVEL_CRITICAL" + } + }, + { + "uuid": "dependency", + "spec": { + "extra_key": "GHSA-dependency", + "finding_tags": ["FINDING_TAGS_REACHABLE_DEPENDENCY"], + "level": "FINDING_LEVEL_MEDIUM" + } + } + ], + "blocking_findings": [{"uuid": "exact"}], + "warning_findings": [{"uuid": "potential"}] + } + } + } +} +JSON`, + ); + + const result = await runEndorPluginScan({ + workspace, + env: { + CODEX_SECURITY_SCAN_CLAWSCAN_COMMAND: command, + CODEX_SECURITY_SCAN_ENDOR_IMAGE: "clawscan-endor:test", + DOCKER_CONFIG: "/tmp/fixture-docker-config", + DOCKER_HOST: "must-not-reach-endor", + ENDOR_NAMESPACE: "fixture-namespace", + ENDOR_TOKEN: "fixture-token", + HOME: "/tmp/fixture-home", + OPENAI_API_KEY: "must-not-reach-endor", + PATH: testPath(workspace), + SECURITY_SCAN_WORKER_TOKEN: "must-not-reach-endor", + }, + }); + + expect(result).toEqual({ + status: "completed", + checkedAt: Date.parse("2026-09-15T00:00:00Z"), + reachableFunctionCount: 1, + findings: [{ severity: "high", summary: "Reachable lodash function is vulnerable" }], + }); + expect( + JSON.parse(await readFile(join(workspace, "endor-artifact", "package.json"), "utf8")), + ).toEqual({ + name: "fixture-plugin", + ...(family === "claw" ? { openclaw: { claw: "CLAW.md" } } : {}), + dependencies: { "runtime-workspace": "workspace:*", lodash: "4.17.20" }, + devDependencies: { typescript: "6.0.3" }, + }); + await expect( + readFile(join(workspace, "endor-artifact", "package-lock.json"), "utf8"), + ).resolves.toContain('"lockfileVersion":3'); + await expect(readFile(join(packageRoot, "npm-shrinkwrap.json"), "utf8")).resolves.toContain( + '"lockfileVersion":3', + ); + expect((await readFile(argsLog, "utf8")).trim().split("\n")).toEqual([ + family === "plugin" ? "./endor-artifact/openclaw.plugin.json" : "./endor-artifact", + "--config", + join(workspace, "endor-clawscan.json"), + "--profile", + "endor", + "--sandbox", + "docker", + "--sandbox-image", + "clawscan-endor:test", + "--output", + join(workspace, "endor-clawscan-artifact.json"), + ]); + const commandEnv = (await readFile(envLog, "utf8")).trim().split("\n"); + expect(commandEnv.slice(0, 7)).toEqual([ + "fixture-namespace", + "fixture-token", + "", + "", + "/tmp/fixture-home", + "/tmp/fixture-docker-config", + "", + ]); + expect(commandEnv[7]).toMatch(/^[a-f0-9]{32}$/); + const config = await readFile(join(workspace, "endor-clawscan.json"), "utf8"); + expect(JSON.parse(config)).toEqual({ + version: 1, + profiles: { + endor: { + scanners: [ + { + id: "endor", + command: "clawhub-endor-scan {{target}}", + targets: ["skill", "plugin"], + env: ["ENDOR_NAMESPACE"], + secretEnv: ["ENDOR_TOKEN"], + }, + ], + }, + }, + }); + expect(config).not.toContain("fixture-token"); + }, + ); + + it("returns an explicit skipped result when the package artifact has no package.json", async () => { + const workspace = await tempDir(); + await mkdir(join(workspace, "artifact"), { recursive: true }); + + await expect(runEndorPluginScan({ workspace, env: {} })).resolves.toMatchObject({ + status: "skipped", + reason: "Endor requires package.json at the package artifact root.", + }); + }); + + it("bounds the summary without losing the total reachable-function count", async () => { + const workspace = await tempDir(); + const packageRoot = join(workspace, "artifact", "package"); + const command = join(workspace, "fake-clawscan"); + await mkdir(packageRoot, { recursive: true }); + await writeFile(join(packageRoot, "package.json"), '{"name":"fixture"}\n'); + const allFindings = Array.from({ length: 52 }, (_, index) => ({ + spec: { + finding_tags: ["FINDING_TAGS_REACHABLE_FUNCTION"], + level: "FINDING_LEVEL_HIGH", + summary: `${index}-${"x".repeat(2_100)}`, + }, + })); + await writeFakeClawScan( + command, + `output="" +while [[ $# -gt 0 ]]; do + if [[ "$1" == "--output" ]]; then output="$2"; shift 2; else shift; fi +done +cat > "$output" <<'JSON' +${JSON.stringify({ + completedAt: "2026-09-16T00:00:00Z", + scanners: { + endor: { + status: "completed", + raw: { all_findings: allFindings, blocking_findings: [], warning_findings: [] }, + }, + }, +})} +JSON`, + ); + + const result = await runEndorPluginScan({ + workspace, + env: { + CODEX_SECURITY_SCAN_CLAWSCAN_COMMAND: command, + CODEX_SECURITY_SCAN_ENDOR_IMAGE: "clawscan-endor:test", + PATH: testPath(workspace), + }, + }); + + expect(result).toMatchObject({ status: "completed", reachableFunctionCount: 52 }); + if (result.status !== "completed") throw new Error("expected a completed Endor analysis"); + expect(result.findings).toHaveLength(50); + expect(result.findings[0]?.summary).toHaveLength(2_000); + }); + + it("preserves a failed scanner artifact cause with configured secrets redacted", async () => { + const workspace = await tempDir(); + const packageRoot = join(workspace, "artifact", "package"); + const command = join(workspace, "fake-clawscan"); + await mkdir(packageRoot, { recursive: true }); + await writeFile(join(packageRoot, "package.json"), '{"name":"fixture"}\n'); + await writeFakeClawScan( + command, + `output="" +while [[ $# -gt 0 ]]; do + if [[ "$1" == "--output" ]]; then output="$2"; shift 2; else shift; fi +done +cat > "$output" <<'JSON' +{"completedAt":"2026-09-16T00:00:00Z","scanners":{"endor":{"status":"failed","raw":null,"error":"Dependency resolution failed with ENDOR_TOKEN=fixture-token and fixture-token"}}} +JSON`, + ); + const diagnostics: Array> = []; + + await expect( + runEndorPluginScan({ + workspace, + env: { + CODEX_SECURITY_SCAN_CLAWSCAN_COMMAND: command, + CODEX_SECURITY_SCAN_ENDOR_IMAGE: "clawscan-endor:test", + ENDOR_NAMESPACE: "fixture-namespace", + ENDOR_TOKEN: "fixture-token", + PATH: testPath(workspace), + }, + onDiagnostic: (next) => diagnostics.push(next), + }), + ).rejects.toThrow( + "Endor ClawScan scanner status was failed: Dependency resolution failed with ENDOR_TOKEN=[redacted-secret] and [redacted-secret]", + ); + + expect(Object.assign({}, ...diagnostics)).toMatchObject({ + exitCode: 0, + scannerError: + "Dependency resolution failed with ENDOR_TOKEN=[redacted-secret] and [redacted-secret]", + }); + expect(JSON.stringify(diagnostics)).not.toContain("fixture-token"); + }); + + it.each([ + { + name: "exit", + body: 'echo "ENDOR_TOKEN=fixture-token" >&2\nexit 17', + stderr: "ENDOR_TOKEN=[redacted-secret]\n", + timedOut: false, + }, + { + name: "timeout", + body: "sleep 2", + stderr: "", + timedOut: true, + }, + ])("preserves redacted command diagnostics on $name", async ({ body, stderr, timedOut }) => { + const workspace = await tempDir(); + const packageRoot = join(workspace, "artifact", "package"); + const command = join(workspace, "fake-clawscan"); + await mkdir(packageRoot, { recursive: true }); + await writeFile(join(packageRoot, "package.json"), '{"name":"fixture"}\n'); + await writeFakeClawScan(command, body); + const diagnostics: Array> = []; + + await expect( + runEndorPluginScan({ + workspace, + env: { + CODEX_SECURITY_SCAN_CLAWSCAN_COMMAND: command, + CODEX_SECURITY_SCAN_ENDOR_IMAGE: "clawscan-endor:test", + CODEX_SECURITY_SCAN_ENDOR_TIMEOUT_MS: timedOut ? "1000" : undefined, + ENDOR_NAMESPACE: "fixture-namespace", + ENDOR_TOKEN: "fixture-token", + PATH: testPath(workspace), + }, + onDiagnostic: (next) => diagnostics.push(next), + }), + ).rejects.toThrow(timedOut ? "Endor ClawScan timed out" : "Endor ClawScan exited 17"); + + expect(Object.assign({}, ...diagnostics)).toMatchObject({ + stderr, + timedOut, + }); + expect(JSON.stringify(diagnostics)).not.toContain("fixture-token"); + }); + + it("removes an owned late-created container after the parent timeout", async () => { + const workspace = await tempDir(); + const packageRoot = join(workspace, "artifact", "package"); + const command = join(workspace, "fake-clawscan"); + const runIdPath = join(workspace, "run-id.txt"); + const listCountPath = join(workspace, "list-count.txt"); + const removedPath = join(workspace, "removed.txt"); + const cleanupEnvPath = join(workspace, "cleanup-env.log"); + await mkdir(packageRoot, { recursive: true }); + await writeFile(join(packageRoot, "package.json"), '{"name":"fixture"}\n'); + await writeFakeClawScan(command, "sleep 5"); + await writeFakeDocker( + workspace, + `printf '%s\\n' "\${ENDOR_NAMESPACE-}|\${ENDOR_TOKEN-}|\${ENDOR_API_CREDENTIALS_KEY-}|\${ENDOR_API_CREDENTIALS_SECRET-}|\${HOME-}|\${DOCKER_CONFIG-}" >> ${JSON.stringify(cleanupEnvPath)} +case "$2" in + ls) + printf '%s' "\${6##*=}" > ${JSON.stringify(runIdPath)} + count=0 + if [[ -f ${JSON.stringify(listCountPath)} ]]; then count=$(cat ${JSON.stringify(listCountPath)}); fi + count=$((count + 1)) + printf '%s' "$count" > ${JSON.stringify(listCountPath)} + if (( count >= 2 )) && [[ ! -f ${JSON.stringify(removedPath)} ]]; then printf '%s\\n' 0123456789ab; fi + ;; + inspect) + printf '%s\\n%s\\n' "$(cat ${JSON.stringify(runIdPath)})" command123 + ;; + rm) + printf '%s' "$5" > ${JSON.stringify(removedPath)} + ;; + *) exit 99 ;; +esac`, + ); + + const failure = await runEndorPluginScan({ + workspace, + env: { + CODEX_SECURITY_SCAN_CLAWSCAN_COMMAND: command, + CODEX_SECURITY_SCAN_ENDOR_IMAGE: "clawscan-endor:test", + CODEX_SECURITY_SCAN_ENDOR_TIMEOUT_MS: "500", + DOCKER_CONFIG: "/tmp/fixture-docker-config", + ENDOR_API_CREDENTIALS_KEY: "fixture-key", + ENDOR_API_CREDENTIALS_SECRET: "fixture-secret", + ENDOR_NAMESPACE: "fixture-namespace", + ENDOR_TOKEN: "fixture-token", + HOME: "/tmp/fixture-home", + PATH: testPath(workspace), + }, + }).catch((error: unknown) => error); + expect(failure).toBeInstanceOf(Error); + if (!(failure instanceof Error)) throw new Error("expected Endor timeout failure"); + expect(failure.message).toContain("Endor ClawScan timed out"); + expect(failure.message).not.toContain("Docker cleanup failed"); + + expect(await readFile(removedPath, "utf8")).toBe("0123456789ab"); + const cleanupEnvironments = (await readFile(cleanupEnvPath, "utf8")).trim().split("\n"); + expect(cleanupEnvironments.length).toBeGreaterThan(2); + expect(new Set(cleanupEnvironments)).toEqual( + new Set(["||||/tmp/fixture-home|/tmp/fixture-docker-config"]), + ); + }); + + it("reports cleanup failure after command success and alongside command failure", async () => { + const tests = [ + { + name: "after success", + body: `output="" +while [[ $# -gt 0 ]]; do + if [[ "$1" == "--output" ]]; then output="$2"; shift 2; else shift; fi +done +cat > "$output" <<'JSON' +{"completedAt":"2026-09-16T00:00:00Z","scanners":{"endor":{"status":"completed","raw":{"all_findings":[],"blocking_findings":[],"warning_findings":[]}}}} +JSON`, + expected: "Endor ClawScan Docker cleanup failed", + }, + { + name: "with command failure", + body: "exit 17", + expected: + "Endor ClawScan failed: Endor ClawScan exited 17; see redacted stdout/stderr diagnostics; Docker cleanup failed", + }, + ]; + for (const test of tests) { + const workspace = await tempDir(); + const packageRoot = join(workspace, "artifact", "package"); + const command = join(workspace, "fake-clawscan"); + await mkdir(packageRoot, { recursive: true }); + await writeFile(join(packageRoot, "package.json"), '{"name":"fixture"}\n'); + await writeFakeClawScan(command, test.body); + await writeFakeDocker(workspace, "exit 23"); + + await expect( + runEndorPluginScan({ + workspace, + env: { + CODEX_SECURITY_SCAN_CLAWSCAN_COMMAND: command, + CODEX_SECURITY_SCAN_ENDOR_IMAGE: "clawscan-endor:test", + ENDOR_NAMESPACE: "fixture-namespace", + ENDOR_TOKEN: "fixture-token", + PATH: testPath(workspace), + }, + }), + test.name, + ).rejects.toThrow(test.expected); + } + }); + + it("accepts auto-remove races and rejects malformed container IDs", async () => { + const tests = [ + { + name: "missing during inspect", + docker: () => `case "$2" in + ls) printf '%s\\n' 0123456789ab ;; + inspect) echo 'Error: No such object: 0123456789ab' >&2; exit 1 ;; + *) exit 99 ;; +esac`, + }, + { + name: "missing during removal", + docker: (runIdPath: string) => `case "$2" in + ls) printf '%s\\n' 0123456789ab ;; + inspect) printf '%s\\n%s\\n' "$(cat ${JSON.stringify(runIdPath)})" command123 ;; + rm) echo 'Error: No such container: 0123456789ab' >&2; exit 1 ;; + *) exit 99 ;; +esac`, + }, + { + name: "malformed ID", + docker: () => `if [[ "$2" == "ls" ]]; then printf '%s\\n' --force; else exit 99; fi`, + expected: "refusing invalid Docker container ID", + }, + ]; + for (const test of tests) { + const workspace = await tempDir(); + const packageRoot = join(workspace, "artifact", "package"); + const command = join(workspace, "fake-clawscan"); + const runIdPath = join(workspace, "run-id.txt"); + await mkdir(packageRoot, { recursive: true }); + await writeFile(join(packageRoot, "package.json"), '{"name":"fixture"}\n'); + await writeFakeClawScan( + command, + `printf '%s' "$CLAWSCAN_SANDBOX_RUN_ID" > ${JSON.stringify(runIdPath)} +output="" +while [[ $# -gt 0 ]]; do + if [[ "$1" == "--output" ]]; then output="$2"; shift 2; else shift; fi +done +cat > "$output" <<'JSON' +{"completedAt":"2026-09-16T00:00:00Z","scanners":{"endor":{"status":"completed","raw":{"all_findings":[],"blocking_findings":[],"warning_findings":[]}}}} +JSON`, + ); + await writeFakeDocker(workspace, test.docker(runIdPath)); + + const promise = runEndorPluginScan({ + workspace, + env: { + CODEX_SECURITY_SCAN_CLAWSCAN_COMMAND: command, + CODEX_SECURITY_SCAN_ENDOR_IMAGE: "clawscan-endor:test", + ENDOR_NAMESPACE: "fixture-namespace", + ENDOR_TOKEN: "fixture-token", + PATH: testPath(workspace), + }, + }); + if (test.expected) await expect(promise, test.name).rejects.toThrow(test.expected); + else + await expect(promise, test.name).resolves.toMatchObject({ + status: "completed", + reachableFunctionCount: 0, + findings: [], + }); + } + }); + + it("rejects an external artifact symlink before invoking ClawScan", async () => { + const workspace = await tempDir(); + const packageRoot = join(workspace, "artifact", "package"); + const external = join(await tempDir(), "outside.js"); + await mkdir(packageRoot, { recursive: true }); + await writeFile(join(packageRoot, "package.json"), '{"name":"fixture"}\n'); + await writeFile(external, "export const outside = true;\n"); + await symlink(external, join(packageRoot, "outside.js")); + + await expect( + runEndorPluginScan({ + workspace, + env: { + CODEX_SECURITY_SCAN_CLAWSCAN_COMMAND: join(workspace, "missing-clawscan"), + CODEX_SECURITY_SCAN_ENDOR_IMAGE: "clawscan-endor:test", + ENDOR_NAMESPACE: "fixture-namespace", + ENDOR_TOKEN: "fixture-token", + }, + }), + ).rejects.toThrow("Package artifact contains a symlink outside its root"); + }); +}); diff --git a/scripts/security/run-endor-plugin-scan.ts b/scripts/security/run-endor-plugin-scan.ts new file mode 100644 index 0000000000..ae66f3f5cc --- /dev/null +++ b/scripts/security/run-endor-plugin-scan.ts @@ -0,0 +1,517 @@ +import { randomUUID } from "node:crypto"; +import { cp, lstat, readFile, readdir, readlink, rename, writeFile } from "node:fs/promises"; +import { dirname, join, relative, resolve, sep } from "node:path"; +import { z } from "zod"; +import type { EndorAnalysis } from "../../convex/lib/endorAnalysis"; +import { CommandFailure, runWorkerCommand } from "../lib/runWorkerCommand"; +import { redactWorkerPublicText } from "../lib/workerRedaction"; +import { resolveClawScanTargetForRoot } from "./clawScanTarget"; + +const ENDOR_ENV_KEYS = [ + "ENDOR_API", + "ENDOR_NAMESPACE", + "ENDOR_TOKEN", + "ENDOR_API_CREDENTIALS_KEY", + "ENDOR_API_CREDENTIALS_SECRET", +] as const; +const CHILD_RUNTIME_ENV_KEYS = [ + "PATH", + "LANG", + "LC_ALL", + "LC_CTYPE", + "TZ", + "SYSTEMROOT", + "SystemRoot", + "WINDIR", + "COMSPEC", + "PATHEXT", +] as const; +const DEFAULT_ENDOR_TIMEOUT_MS = 20 * 60 * 1000; +const DOCKER_CLEANUP_TIMEOUT_MS = 10_000; +const DOCKER_LATE_CREATE_POLL_MS = 100; +const DOCKER_LATE_CREATE_WINDOW_MS = DOCKER_CLEANUP_TIMEOUT_MS - DOCKER_LATE_CREATE_POLL_MS; +const SANDBOX_RUN_ID_ENV = "CLAWSCAN_SANDBOX_RUN_ID"; +const SANDBOX_RUN_ID_LABEL = "org.openclaw.clawscan.run-id"; +const SANDBOX_COMMAND_ID_LABEL = "org.openclaw.clawscan.command-id"; +const MAX_ENDOR_DIAGNOSTIC_CHARS = 20_000; +const MAX_SUMMARY_FINDINGS = 50; +const MAX_SUMMARY_TEXT_CHARS = 2_000; +const REACHABLE_FUNCTION_TAG = "FINDING_TAGS_REACHABLE_FUNCTION"; + +const unknownRecordSchema = z.record(z.string(), z.unknown()); +const endorFindingSchema = z + .object({ + meta: z + .object({ + description: z.string().optional(), + name: z.string().optional(), + }) + .passthrough() + .optional(), + spec: z + .object({ + extra_key: z.string().optional(), + finding_tags: z.array(z.string()).optional(), + level: z.string().optional(), + summary: z.string().optional(), + target_dependency_package_name: z.string().optional(), + target_dependency_version: z.string().optional(), + }) + .passthrough(), + }) + .passthrough(); +const endorRawReportSchema = z + .object({ + all_findings: z.array(endorFindingSchema), + blocking_findings: z.array(z.record(z.string(), z.unknown())), + warning_findings: z.array(z.record(z.string(), z.unknown())), + }) + .passthrough(); +const endorArtifactSchema = z + .object({ + completedAt: z.string().min(1), + scanners: z + .object({ + endor: z + .object({ + error: z.string().optional(), + raw: endorRawReportSchema.nullish(), + status: z.string(), + }) + .passthrough(), + }) + .passthrough(), + }) + .passthrough(); + +export type EndorCommandDiagnostic = { + args?: string[]; + artifactPath?: string; + exitCode?: number | null; + rawArtifact?: string; + scannerError?: string; + sandboxRunId?: string; + stderr?: string; + stdout?: string; + timedOut?: boolean; +}; + +function isPathWithin(root: string, candidate: string) { + const normalizedRoot = resolve(root); + const normalizedCandidate = resolve(candidate); + return ( + normalizedCandidate === normalizedRoot || + normalizedCandidate.startsWith(`${normalizedRoot}${sep}`) + ); +} + +async function assertNoExternalSymlinks(root: string, directory = root): Promise { + for (const entry of await readdir(directory, { withFileTypes: true })) { + const path = join(directory, entry.name); + if (entry.isSymbolicLink()) { + const target = resolve(dirname(path), await readlink(path)); + if (!isPathWithin(root, target)) { + throw new Error("Package artifact contains a symlink outside its root"); + } + continue; + } + if (entry.isDirectory()) await assertNoExternalSymlinks(root, path); + } +} + +async function isRegularFile(path: string) { + return (await lstat(path).catch(() => null))?.isFile() === true; +} + +function packageRootLabel(scanRoot: string, manifestDirectory: string) { + return relative(scanRoot, manifestDirectory).replaceAll("\\", "/") || "."; +} + +async function normalizePackageRoot(scanRoot: string, manifestDirectory: string) { + const packageRoot = packageRootLabel(scanRoot, manifestDirectory); + const manifestPath = join(manifestDirectory, "package.json"); + let parsedManifest: unknown; + try { + parsedManifest = JSON.parse(await readFile(manifestPath, "utf8")); + } catch (error) { + throw new Error(`Endor scan package.json is invalid at ${packageRoot}`, { cause: error }); + } + const manifest = unknownRecordSchema.parse(parsedManifest); + const devDependenciesValue = manifest.devDependencies; + if (devDependenciesValue !== undefined) { + const devDependencies = unknownRecordSchema.safeParse(devDependenciesValue); + if (!devDependencies.success) { + throw new Error(`Endor scan devDependencies is invalid at ${packageRoot}`); + } + const workspaceDependencyNames = Object.entries(devDependencies.data) + .filter(([, version]) => typeof version === "string" && version.startsWith("workspace:")) + .map(([name]) => name) + .sort(); + if (workspaceDependencyNames.length > 0) { + for (const name of workspaceDependencyNames) delete devDependencies.data[name]; + manifest.devDependencies = devDependencies.data; + await writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, "utf8"); + } + } + + const shrinkwrapPath = join(manifestDirectory, "npm-shrinkwrap.json"); + const packageLockPath = join(manifestDirectory, "package-lock.json"); + if ((await isRegularFile(shrinkwrapPath)) && !(await isRegularFile(packageLockPath))) { + await rename(shrinkwrapPath, packageLockPath); + } +} + +async function normalizePackageTree(scanRoot: string, directory = scanRoot): Promise { + const entries = await readdir(directory, { withFileTypes: true }); + if (await isRegularFile(join(directory, "package.json"))) { + await normalizePackageRoot(scanRoot, directory); + } + for (const entry of entries) { + if (!entry.isDirectory() || entry.name === ".git" || entry.name === "node_modules") continue; + await normalizePackageTree(scanRoot, join(directory, entry.name)); + } +} + +function endorTimeoutMs(env: NodeJS.ProcessEnv) { + const parsed = Number(env.CODEX_SECURITY_SCAN_ENDOR_TIMEOUT_MS); + return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_ENDOR_TIMEOUT_MS; +} + +function endorRuntimeEnv( + workspace: string, + source: NodeJS.ProcessEnv, + extraKeys: readonly string[] = [], +) { + const env: NodeJS.ProcessEnv = { + NO_COLOR: "1", + TEMP: workspace, + TMP: workspace, + TMPDIR: workspace, + }; + for (const key of [...CHILD_RUNTIME_ENV_KEYS, ...extraKeys]) { + const value = source[key]; + if (value !== undefined) env[key] = value; + } + // The host Docker CLI needs its selected context. ClawScan independently + // forwards only the trusted custom-scanner config's env into the container. + for (const key of ["HOME", "DOCKER_CONFIG"] as const) { + const value = source[key]; + if (value !== undefined) env[key] = value; + } + return env; +} + +function errorMessage(error: unknown) { + return error instanceof Error ? error.message : String(error); +} + +function dockerObjectMissing(error: unknown) { + if (!(error instanceof CommandFailure)) return false; + const output = `${error.stdout}\n${error.stderr}`.toLowerCase(); + return output.includes("no such object") || output.includes("no such container"); +} + +async function cleanupEndorSandboxContainers(input: { + env: NodeJS.ProcessEnv; + runId: string; + waitForLateCreate: boolean; + workspace: string; +}) { + const deadline = Date.now() + DOCKER_CLEANUP_TIMEOUT_MS; + const lateCreateDeadline = input.waitForLateCreate + ? Date.now() + DOCKER_LATE_CREATE_WINDOW_MS + : Date.now(); + const cleanupEnv = endorRuntimeEnv(input.workspace, input.env); + const remainingMs = () => { + const remaining = deadline - Date.now(); + if (remaining <= 0) throw new Error("Endor ClawScan Docker cleanup timed out"); + return remaining; + }; + const runDocker = async (args: string[]) => + await runWorkerCommand("docker", args, { + commandLabel: "Endor ClawScan Docker cleanup", + cwd: input.workspace, + env: cleanupEnv, + timeoutMs: remainingMs(), + }); + const sweep = async () => { + const listed = await runDocker([ + "container", + "ls", + "--all", + "--quiet", + "--filter", + `label=${SANDBOX_RUN_ID_LABEL}=${input.runId}`, + ]); + const ids = listed.stdout.split(/\s+/).filter(Boolean); + for (const id of ids) { + if (!/^[a-f0-9]{12,64}$/.test(id)) { + throw new Error(`refusing invalid Docker container ID ${JSON.stringify(id)}`); + } + let inspected; + try { + inspected = await runDocker([ + "container", + "inspect", + "--format", + `{{ index .Config.Labels ${JSON.stringify(SANDBOX_RUN_ID_LABEL)} }}\n{{ index .Config.Labels ${JSON.stringify(SANDBOX_COMMAND_ID_LABEL)} }}`, + id, + ]); + } catch (error) { + if (dockerObjectMissing(error)) continue; + throw error; + } + const [runId, commandId, ...extra] = inspected.stdout.trim().split("\n"); + if ( + runId !== input.runId || + !commandId || + extra.length > 0 || + !/^[A-Za-z0-9][A-Za-z0-9_.-]{0,63}$/.test(commandId) + ) { + throw new Error( + `refusing to remove Docker container ${id}: ownership labels do not match (run=${JSON.stringify(runId)}, command=${JSON.stringify(commandId)})`, + ); + } + try { + await runDocker(["container", "rm", "--force", "--volumes", id]); + } catch (error) { + if (dockerObjectMissing(error)) continue; + throw error; + } + } + return ids.length > 0; + }; + + if (await sweep()) return; + while (Date.now() < lateCreateDeadline) { + await new Promise((resolvePromise) => setTimeout(resolvePromise, DOCKER_LATE_CREATE_POLL_MS)); + if (await sweep()) return; + } +} + +function redactEndorDiagnosticText(value: string, env: NodeJS.ProcessEnv) { + let redacted = value; + for (const key of [ + "ENDOR_TOKEN", + "ENDOR_API_CREDENTIALS_KEY", + "ENDOR_API_CREDENTIALS_SECRET", + ] as const) { + const secret = env[key]; + if (secret) redacted = redacted.replaceAll(secret, "[redacted-secret]"); + } + return redactWorkerPublicText(redacted, MAX_ENDOR_DIAGNOSTIC_CHARS); +} + +function normalizeSeverity(value: string | undefined) { + return ( + value + ?.replace(/^FINDING_LEVEL_/, "") + .trim() + .toLowerCase() || "unknown" + ).slice(0, 64); +} + +function normalizeFindingSummary(finding: z.infer) { + const advisory = finding.spec.extra_key?.trim(); + const dependency = finding.spec.target_dependency_package_name?.trim(); + const dependencyVersion = finding.spec.target_dependency_version?.trim(); + const dependencyLabel = + dependency && dependencyVersion && !dependency.endsWith(`@${dependencyVersion}`) + ? `${dependency} ${dependencyVersion}` + : dependency || dependencyVersion; + const reportedSummary = + finding.spec.summary?.trim() || finding.meta?.description?.trim() || finding.meta?.name?.trim(); + return ( + reportedSummary || + [advisory, dependencyLabel].filter(Boolean).join(" in ") || + "Endor reported a reachable vulnerable function" + ).slice(0, MAX_SUMMARY_TEXT_CHARS); +} + +function completedAtMs(value: string) { + const checkedAt = Date.parse(value); + if (!Number.isFinite(checkedAt)) throw new Error("Endor ClawScan completedAt was invalid"); + return checkedAt; +} + +async function resolvePackageArtifactRoot(workspace: string) { + for (const candidate of [join(workspace, "artifact", "package"), join(workspace, "artifact")]) { + if (await isRegularFile(join(candidate, "package.json"))) return candidate; + } + return undefined; +} + +export function isEndorPluginScanEnabled(env: NodeJS.ProcessEnv = process.env) { + return env.CODEX_SECURITY_SCAN_ENDOR_ENABLED === "1"; +} + +export async function runEndorPluginScan(input: { + env?: NodeJS.ProcessEnv; + onDiagnostic?: (diagnostic: Partial) => void; + workspace: string; +}): Promise { + const env = input.env ?? process.env; + const sourceRoot = await resolvePackageArtifactRoot(input.workspace); + if (!sourceRoot) { + return { + status: "skipped", + checkedAt: Date.now(), + reason: "Endor requires package.json at the package artifact root.", + }; + } + + const image = env.CODEX_SECURITY_SCAN_ENDOR_IMAGE?.trim(); + if (!image) throw new Error("CODEX_SECURITY_SCAN_ENDOR_IMAGE is required when Endor is enabled"); + await assertNoExternalSymlinks(sourceRoot); + const scanRoot = join(input.workspace, "endor-artifact"); + await cp(sourceRoot, scanRoot, { + recursive: true, + errorOnExist: true, + force: false, + verbatimSymlinks: true, + }); + await normalizePackageTree(scanRoot); + const outputPath = join(input.workspace, "endor-clawscan-artifact.json"); + const configPath = join(input.workspace, "endor-clawscan.json"); + await writeFile( + configPath, + JSON.stringify({ + version: 1, + profiles: { + endor: { + scanners: [ + { + id: "endor", + command: "clawhub-endor-scan {{target}}", + // ClawScan classifies CLAW.md package releases as skill targets. + targets: ["skill", "plugin"], + env: ["ENDOR_NAMESPACE", ...(env.ENDOR_API?.trim() ? ["ENDOR_API"] : [])], + secretEnv: env.ENDOR_TOKEN?.trim() + ? ["ENDOR_TOKEN"] + : ["ENDOR_API_CREDENTIALS_KEY", "ENDOR_API_CREDENTIALS_SECRET"], + }, + ], + }, + }, + }), + "utf8", + ); + const command = env.CODEX_SECURITY_SCAN_CLAWSCAN_COMMAND?.trim() || "clawscan"; + const target = await resolveClawScanTargetForRoot({ + artifactKind: "packageRelease", + root: "./endor-artifact", + workspace: input.workspace, + }); + const args = [ + target, + "--config", + configPath, + "--profile", + "endor", + "--sandbox", + "docker", + "--sandbox-image", + image, + "--output", + outputPath, + ]; + const sandboxRunId = randomUUID().replaceAll("-", ""); + input.onDiagnostic?.({ + args: [command, ...args], + artifactPath: outputPath, + sandboxRunId, + }); + const commandEnv = endorRuntimeEnv(input.workspace, env, ENDOR_ENV_KEYS); + commandEnv[SANDBOX_RUN_ID_ENV] = sandboxRunId; + + const captureArtifact = async () => { + const rawArtifact = await readFile(outputPath, "utf8").catch(() => undefined); + if (rawArtifact === undefined) return undefined; + input.onDiagnostic?.({ rawArtifact: redactEndorDiagnosticText(rawArtifact, env) }); + try { + const parsed = endorArtifactSchema.safeParse(JSON.parse(rawArtifact) as unknown); + return parsed.success ? parsed.data : undefined; + } catch { + return undefined; + } + }; + + let commandError: unknown; + try { + const output = await runWorkerCommand(command, args, { + commandLabel: "Endor ClawScan", + cwd: input.workspace, + env: commandEnv, + timeoutMs: endorTimeoutMs(env), + }); + input.onDiagnostic?.({ + exitCode: 0, + stderr: redactEndorDiagnosticText(output.stderr, env), + stdout: redactEndorDiagnosticText(output.stdout, env), + }); + } catch (error) { + commandError = error; + if (error instanceof CommandFailure) { + input.onDiagnostic?.({ + exitCode: error.exitCode, + stderr: redactEndorDiagnosticText(error.stderr, env), + stdout: redactEndorDiagnosticText(error.stdout, env), + timedOut: error.timedOut, + }); + } + await captureArtifact(); + } + + let cleanupError: unknown; + try { + await cleanupEndorSandboxContainers({ + env, + runId: sandboxRunId, + waitForLateCreate: commandError instanceof CommandFailure && commandError.timedOut, + workspace: input.workspace, + }); + } catch (error) { + cleanupError = error; + } + if (commandError !== undefined && cleanupError !== undefined) { + throw new AggregateError( + [commandError, cleanupError], + `Endor ClawScan failed: ${errorMessage(commandError)}; Docker cleanup failed: ${errorMessage(cleanupError)}`, + ); + } + if (commandError !== undefined) throw commandError; + if (cleanupError !== undefined) { + throw new Error(`Endor ClawScan Docker cleanup failed: ${errorMessage(cleanupError)}`, { + cause: cleanupError, + }); + } + + const artifact = await captureArtifact(); + if (!artifact) throw new Error("Endor ClawScan did not emit a valid JSON artifact"); + if (artifact.scanners.endor.status !== "completed") { + const scannerError = artifact.scanners.endor.error + ? redactEndorDiagnosticText(artifact.scanners.endor.error, env) + : undefined; + input.onDiagnostic?.({ scannerError }); + throw new Error( + `Endor ClawScan scanner status was ${artifact.scanners.endor.status}${scannerError ? `: ${scannerError}` : ""}`, + ); + } + if (!artifact.scanners.endor.raw) { + throw new Error("Endor ClawScan scanner output was missing"); + } + const checkedAt = completedAtMs(artifact.completedAt); + const reachableFindings = artifact.scanners.endor.raw.all_findings.filter((finding) => + finding.spec.finding_tags?.includes(REACHABLE_FUNCTION_TAG), + ); + const findings = reachableFindings.slice(0, MAX_SUMMARY_FINDINGS).map((finding) => ({ + severity: normalizeSeverity(finding.spec.level), + summary: normalizeFindingSummary(finding), + })); + return { + status: "completed", + checkedAt, + reachableFunctionCount: reachableFindings.length, + findings, + }; +} diff --git a/scripts/security/security-scan-worker-workflow.test.ts b/scripts/security/security-scan-worker-workflow.test.ts index 6984149750..d37c99a917 100644 --- a/scripts/security/security-scan-worker-workflow.test.ts +++ b/scripts/security/security-scan-worker-workflow.test.ts @@ -144,6 +144,10 @@ describe("security-scan-codex workflow", () => { ]); expectSecretStepAllowlist(steps, "LLM_API_KEY", ["Run Codex security worker"]); expectSecretStepAllowlist(steps, "SECURITY_SCAN_WORKER_TOKEN", ["Run Codex security worker"]); + expectSecretStepAllowlist(steps, "ENDOR_API_CREDENTIALS_KEY", ["Run Codex security worker"]); + expectSecretStepAllowlist(steps, "ENDOR_API_CREDENTIALS_SECRET", ["Run Codex security worker"]); + expect(jobEnv).not.toHaveProperty("ENDOR_API_CREDENTIALS_KEY"); + expect(jobEnv).not.toHaveProperty("ENDOR_API_CREDENTIALS_SECRET"); expectSecretStepAllowlist(steps, "VT_API_KEY", []); expect(scanStep?.env ?? {}).not.toHaveProperty("CODEX_API_KEY"); expect(scanStep?.env ?? {}).not.toHaveProperty("OPENAI_API_KEY"); @@ -157,8 +161,18 @@ describe("security-scan-codex workflow", () => { const skillspectorInstall = steps.find((step) => step.name === "Install SkillSpector")?.run; expect(codexInstall).toContain("npm install -g @openai/codex@0.142.3"); expect(codexInstall).not.toContain("@latest"); - expect(clawScanInstall).toContain("npm install -g @openclaw/clawscan@0.1.8"); + expect(jobEnv.CODEX_SECURITY_SCAN_CLAWSCAN_VERSION).toBe( + "${{ vars.CODEX_SECURITY_SCAN_CLAWSCAN_VERSION || '0.2.0' }}", + ); + expect(clawScanInstall).toContain( + 'npm install -g "@openclaw/clawscan@$CODEX_SECURITY_SCAN_CLAWSCAN_VERSION"', + ); expect(clawScanInstall).not.toContain("@latest"); + const endorPrepare = steps.find((step) => step.name === "Prepare Endor scanner"); + expect(endorPrepare?.if).toBe("${{ env.CODEX_SECURITY_SCAN_ENDOR_ENABLED == '1' }}"); + expect(endorPrepare?.run).toContain("test -x /usr/local/bin/clawhub-endor-scan"); + expect(endorPrepare?.run).toContain("@sha256:[a-f0-9]{64}$"); + expect(endorPrepare?.run).toContain('docker pull "$CODEX_SECURITY_SCAN_ENDOR_IMAGE"'); expect(aigInstall).toContain( "python -m pip install --require-hashes -r scripts/security/aig-worker-requirements.txt", ); @@ -180,6 +194,10 @@ describe("security-scan-codex workflow", () => { LLM_API_KEY: "${{ secrets.OPENAI_API_KEY || secrets.CODEX_API_KEY }}", OPENAI_API_KEY: "${{ secrets.OPENAI_API_KEY }}", SECURITY_SCAN_WORKER_TOKEN: "${{ secrets.SECURITY_SCAN_WORKER_TOKEN }}", + ENDOR_NAMESPACE: "${{ vars.ENDOR_NAMESPACE }}", + ENDOR_API: "${{ vars.ENDOR_API }}", + ENDOR_API_CREDENTIALS_KEY: "${{ secrets.ENDOR_API_CREDENTIALS_KEY }}", + ENDOR_API_CREDENTIALS_SECRET: "${{ secrets.ENDOR_API_CREDENTIALS_SECRET }}", }); }); }); diff --git a/specs/endor-plugin-scans.md b/specs/endor-plugin-scans.md new file mode 100644 index 0000000000..8a2551205e --- /dev/null +++ b/specs/endor-plugin-scans.md @@ -0,0 +1,97 @@ +# Endor in the plugin scan pipeline + +## Intent + +Plugin publication and rescan requests enqueue work and return without waiting for +Endor's dependency and function-reachability analysis. Endor runs in the existing +ClawHub security worker, alongside the existing ClawScan profile. There is one +queue, one lease per release scan, and one completion submission. + +New publications, owner-requested rescans, and admin bulk plugin rescans all use +this path. The existing dispatch watchdog and expired-lease recovery service the +queue. This change does not add a periodic full-catalog LLM rescan. Existing +plugins can be queued through the admin plugin bulk-rescan command, which already +supports paced batches and preserves active jobs. + +## Result contract + +- Apply Endor to package-release jobs. Skill scans keep their existing scanner set. +- Use ClawScan's custom command scanner to run the ClawHub-owned Endor wrapper + in Docker. The normal ClawHub profile + continues to use its existing execution configuration. +- Keep Endor separate from the ClawScan judge and the existing moderation verdict. + Dependency vulnerabilities are supplemental findings; they do not independently + quarantine a package or change its download policy. +- Display only findings tagged `FINDING_TAGS_REACHABLE_FUNCTION`. A reachable + dependency or a potentially reachable function does not satisfy that filter. +- Store a bounded summary on the exact package release. Keep the total count + when the displayed list is capped. Package jobs do not upload or retain full + scanner reports; worker diagnostics remain bounded and redacted. +- An unsupported package is explicitly not analyzed. An Endor failure is saved + as a failed analysis with a safe reason, never a successful empty report. + The primary moderation result still completes and can quarantine the release. + An Endor failure does not retry the whole job; owners or admins can request a + rescan. Primary scanner failures retain the existing job failure/retry path. +- Preserve the last stored result while replacement work is queued or running. + Its check time identifies the analysis being displayed. +- Settle both concurrent scan processes before deleting their workspace. + +The immutable uploaded artifact remains the source of truth. Endor's disposable +copy may normalize an npm shrinkwrap filename and remove unresolved workspace +development dependencies. Runtime dependencies remain unchanged. + +## Hosted rollout + +The worker is off for Endor until `CODEX_SECURITY_SCAN_ENDOR_ENABLED=1` is set. +Enable it only after the backend result contract is deployed and the following +worker configuration is available: + +- `CODEX_SECURITY_SCAN_CLAWSCAN_VERSION`: an exact released ClawScan version with + custom command scanners and sandbox ownership labels. The default is `0.2.0`. + ClawHub uses those labels to clean up containers after a worker timeout. + Endor needs no built-in adapter. +- `CODEX_SECURITY_SCAN_ENDOR_IMAGE`: the Endor scanner Docker image pinned by its + SHA-256 digest. Run the manual `Endor Scanner Image` workflow from `main` to + publish `ghcr.io/openclaw/clawhub-endor`; copy the digest from its job summary. + It builds `scripts/security/endor/Dockerfile` with the pinned Endor CLI and + verifies its checksum. The package must grant this repository Actions access. +- `ENDOR_NAMESPACE` and the `ENDOR_API_CREDENTIALS_KEY` / + `ENDOR_API_CREDENTIALS_SECRET` secrets. `ENDOR_API` is optional. + +The worker authenticates to GHCR with its read-only package token, pulls the +pinned image, and removes that login before claiming work. +The worker writes a trusted scanner profile outside the submitted artifact; +it contains credential names only. Credentials are available only to the worker step; the Endor +subprocess receives its own credentials, and the main scan does not inherit them. + +The wrapper isolates npm/Yarn from scanner credentials, disables dependency +scripts and target-selected Yarn executables, rejects `.npmrc`, and scans a fresh +Git snapshot. It preserves findings JSON, converts successful empty output to an +empty report, accepts Endor's policy exit 128, and rejects analysis errors even +when Endor exits zero. The image retains the reviewed npm version that prevents +Git dependency prepare scripts from bypassing script suppression. + +Image publication, hosted configuration, and ClawHub deployment remain separate +manual rollout actions. + +### Deployment and rollback order + +1. Deploy the additive backend schema and result handlers first. Existing + releases without Endor fields remain readable, and workers that omit Endor + results can still complete jobs. +2. Configure the released scanner version, image digest, and credentials while + Endor remains disabled. Enable it for a small plugin batch, then inspect the + stored summaries, failures, and queue health before expanding. +3. To stop Endor, disable the worker flag and allow active jobs to finish or stop + their workers. Keep the additive backend schema and result handlers deployed. + The fields remain readable; later scans replace the release's current summary. + This is not a report history archive. + +Do not redeploy the old backend schema after an Endor result has been stored. +Convex rejects those documents because the old schema does not accept +`endorAnalysis`. A complete backend rollback needs +a revision that retains these optional fields and the compatible result +handlers. Do not delete stored results just to make the old schema deployable. + +Endor runs with `--dry-run`; the plugin audit and its report download contain the +saved summary. These scans do not create persistent projects in the Endor dashboard. diff --git a/src/components/EndorSecurityAudit.test.tsx b/src/components/EndorSecurityAudit.test.tsx new file mode 100644 index 0000000000..4fbdb45e0d --- /dev/null +++ b/src/components/EndorSecurityAudit.test.tsx @@ -0,0 +1,83 @@ +import { render, screen } from "@testing-library/react"; +import { describe, expect, it } from "vitest"; +import { buildSecurityAuditExportEntries } from "../lib/securityAuditExport"; +import { SecurityAuditPage } from "./SecurityAuditPage"; + +const entity = { + kind: "plugin" as const, + title: "Slack", + name: "@openclaw/slack", + version: "2026.7.1", + detailPath: "/plugins/@openclaw/slack", +}; + +describe("stored Endor plugin scan", () => { + it("shows the stored reachable finding and includes it in the audit export", () => { + const analysis = { + status: "completed" as const, + checkedAt: Date.parse("2026-09-15T00:00:00Z"), + reachableFunctionCount: 1, + findings: [ + { severity: "FINDING_LEVEL_HIGH", summary: "Axios uploads bypass maxBodyLength." }, + ], + }; + render( + , + ); + expect(screen.getByRole("heading", { name: "Endor dependency reachability" })).toBeTruthy(); + expect(screen.getByText("Axios uploads bypass maxBodyLength.", { exact: false })).toBeTruthy(); + expect(screen.getByText("high")).toBeTruthy(); + expect(screen.getByText("Sep 15, 2026 · 12:00 AM UTC")).toBeTruthy(); + const entries = buildSecurityAuditExportEntries({ entity, endorAnalysis: analysis }); + expect(entries.find((entry) => entry.path === "endor.json")?.value).toEqual(analysis); + }); + + it("keeps unsupported analysis distinct from a completed scan with zero findings", () => { + const view = render( + , + ); + expect(screen.getByText("Not analyzed: No package.json")).toBeTruthy(); + expect(screen.queryByText(/Endor found no vulnerabilities/)).toBeNull(); + view.rerender( + , + ); + expect( + screen.getByText("Endor found no vulnerabilities marked as reachable functions."), + ).toBeTruthy(); + }); + + it("shows an Endor failure and exports it without changing the primary verdict", () => { + const analysis = { + status: "failed" as const, + checkedAt: 1, + reason: "Run a rescan to try again.", + }; + render( + , + ); + expect(screen.getByText("Scan failed: Run a rescan to try again.")).toBeTruthy(); + expect(screen.queryByText(/Endor found no vulnerabilities/)).toBeNull(); + expect(screen.getAllByText("Malicious").length).toBeGreaterThan(0); + const entries = buildSecurityAuditExportEntries({ entity, endorAnalysis: analysis }); + expect(entries.find((entry) => entry.path === "endor.json")?.value).toEqual(analysis); + }); +}); diff --git a/src/components/SecurityAuditPage.tsx b/src/components/SecurityAuditPage.tsx index ab800c9938..19535f7c0f 100644 --- a/src/components/SecurityAuditPage.tsx +++ b/src/components/SecurityAuditPage.tsx @@ -1,4 +1,4 @@ -import { getSecurityAuditOverviewCopy } from "clawhub-schema"; +import { getSecurityAuditOverviewCopy, type PackageEndorAnalysis } from "clawhub-schema"; import { ArrowLeft, Check, Clock, Download, Info, RefreshCw, TriangleAlert } from "lucide-react"; import { useEffect, useState } from "react"; import type { Id } from "../../convex/_generated/dataModel"; @@ -56,6 +56,7 @@ type SecurityAuditPageProps = { aigAnalysis?: AigAnalysis | null; llmAnalysis?: LlmAnalysis | null; skillSpectorAnalysis?: SkillSpectorAnalysis | null; + endorAnalysis?: PackageEndorAnalysis | null; skillSpectorApplicable?: boolean; staticScan?: StaticScan | null; source?: Record | null; @@ -1000,6 +1001,51 @@ export function SecurityAuditPage(props: SecurityAuditPageProps) { {orderedScanners.map((kind) => ( ))} + {props.entity.kind === "plugin" && props.endorAnalysis ? ( +
+
+

+ Endor dependency reachability +

+
+
+

Checked {formatAuditSidebarTime(props.endorAnalysis.checkedAt)}

+ {props.endorAnalysis.status !== "completed" ? ( +

+ {props.endorAnalysis.status === "failed" ? "Scan failed" : "Not analyzed"}:{" "} + {props.endorAnalysis.reason} +

+ ) : ( + <> +

+ {props.endorAnalysis.reachableFunctionCount === 0 + ? "Endor found no vulnerabilities marked as reachable functions." + : `${props.endorAnalysis.reachableFunctionCount} ${props.endorAnalysis.reachableFunctionCount === 1 ? "vulnerability" : "vulnerabilities"} marked as reachable functions by Endor.`} +

+ {props.endorAnalysis.findings.length > 0 ? ( +
    + {props.endorAnalysis.findings.map((finding) => ( +
  • + + {finding.severity.replace("FINDING_LEVEL_", "").toLowerCase()} + {" "} + — {finding.summary} +
  • + ))} +
+ ) : null} + {props.endorAnalysis.reachableFunctionCount > + props.endorAnalysis.findings.length ? ( +

Showing the first {props.endorAnalysis.findings.length} findings.

+ ) : null} + + )} +
+
+ ) : null}