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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion .github/workflows/security-scan-codex.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,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.1.8' }}
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"
Expand Down Expand Up @@ -89,9 +92,18 @@ 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' }}
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; }
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
Expand Down Expand Up @@ -123,6 +135,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" \
Expand Down
4 changes: 4 additions & 0 deletions convex/_generated/api.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
12 changes: 12 additions & 0 deletions convex/devSeed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down
78 changes: 78 additions & 0 deletions convex/httpApiV1.handlers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, unknown>) => {
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",
Expand Down Expand Up @@ -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"],
Expand Down Expand Up @@ -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",
},
Expand Down Expand Up @@ -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",
},
},
};
}
Expand Down Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions convex/httpApiV1/packagesV1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"];
Expand Down Expand Up @@ -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,
};
Expand Down Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions convex/httpApiV1/skillsV1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -482,11 +482,13 @@ function buildSkillScanReportZip(status: Record<string, unknown>) {
"- `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.",
Expand All @@ -497,6 +499,7 @@ function buildSkillScanReportZip(status: Record<string, unknown>) {
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",
Expand Down
21 changes: 21 additions & 0 deletions convex/lib/endorAnalysis.ts
Original file line number Diff line number Diff line change
@@ -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<typeof endorAnalysisSchema>;
Loading
Loading