@oh-my-pi/pi-coding-agent 17.1.8 → 17.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +110 -0
- package/dist/{CHANGELOG-vt8ene9g.md → CHANGELOG-dj46zzrm.md} +110 -0
- package/dist/cli.js +10009 -9654
- package/dist/{template-dys3vk5b.js → template-8vdv6xb4.js} +1 -1
- package/dist/types/advisor/runtime.d.ts +8 -2
- package/dist/types/advisor/transcript-recorder.d.ts +15 -0
- package/dist/types/auto-thinking/classifier.d.ts +9 -2
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/session-picker.d.ts +14 -7
- package/dist/types/collab/guest.d.ts +21 -2
- package/dist/types/commands/launch.d.ts +6 -0
- package/dist/types/config/model-discovery.d.ts +12 -0
- package/dist/types/config/settings-schema.d.ts +73 -8
- package/dist/types/cursor-bridge-tools.d.ts +54 -0
- package/dist/types/cursor.d.ts +193 -8
- package/dist/types/edit/edit-clipboard.d.ts +19 -0
- package/dist/types/edit/hashline/diff.d.ts +9 -1
- package/dist/types/edit/index.d.ts +8 -1
- package/dist/types/edit/streaming.d.ts +8 -1
- package/dist/types/extensibility/extensions/runner.d.ts +19 -2
- package/dist/types/extensibility/extensions/types.d.ts +25 -1
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +25 -0
- package/dist/types/internal-urls/index.d.ts +1 -0
- package/dist/types/internal-urls/local-protocol.d.ts +3 -2
- package/dist/types/internal-urls/security-protocol.d.ts +15 -0
- package/dist/types/internal-urls/types.d.ts +1 -1
- package/dist/types/launch/terminal-output-worker-client.d.ts +5 -0
- package/dist/types/launch/terminal-output-worker-protocol.d.ts +14 -0
- package/dist/types/launch/terminal-output-worker.d.ts +1 -0
- package/dist/types/lsp/client.d.ts +2 -0
- package/dist/types/main.d.ts +2 -0
- package/dist/types/mcp/manager.d.ts +33 -4
- package/dist/types/modes/components/codex-reset-fireworks.d.ts +51 -0
- package/dist/types/modes/components/read-tool-group.d.ts +12 -0
- package/dist/types/modes/components/session-selector.d.ts +6 -0
- package/dist/types/modes/components/status-line/component.d.ts +23 -0
- package/dist/types/modes/components/tool-execution.d.ts +16 -7
- package/dist/types/modes/components/usage-row.d.ts +2 -0
- package/dist/types/modes/controllers/mcp-command-controller.d.ts +26 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +2 -1
- package/dist/types/modes/interactive-mode.d.ts +4 -2
- package/dist/types/modes/rpc/rpc-client.d.ts +7 -0
- package/dist/types/modes/rpc/rpc-types.d.ts +16 -0
- package/dist/types/modes/theme/theme.d.ts +12 -1
- package/dist/types/modes/types.d.ts +2 -1
- package/dist/types/sdk.d.ts +18 -0
- package/dist/types/security/auth.d.ts +30 -0
- package/dist/types/security/cloud.d.ts +79 -0
- package/dist/types/security/comparison.d.ts +49 -0
- package/dist/types/security/contracts/ids.d.ts +15 -0
- package/dist/types/security/contracts/index.d.ts +4 -0
- package/dist/types/security/contracts/schemas.d.ts +660 -0
- package/dist/types/security/contracts/types.d.ts +234 -0
- package/dist/types/security/contracts/validation.d.ts +5 -0
- package/dist/types/security/coordinator.d.ts +100 -0
- package/dist/types/security/importers/codex-security.d.ts +7 -0
- package/dist/types/security/importers/index.d.ts +2 -0
- package/dist/types/security/importers/sarif.d.ts +9 -0
- package/dist/types/security/index.d.ts +13 -0
- package/dist/types/security/preflight.d.ts +61 -0
- package/dist/types/security/provenance.d.ts +24 -0
- package/dist/types/security/publication.d.ts +78 -0
- package/dist/types/security/remediation.d.ts +27 -0
- package/dist/types/security/resource-output.d.ts +8 -0
- package/dist/types/security/sarif.d.ts +2 -0
- package/dist/types/security/store.d.ts +40 -0
- package/dist/types/session/agent-session-events.d.ts +2 -0
- package/dist/types/session/agent-session-types.d.ts +36 -1
- package/dist/types/session/agent-session.d.ts +4 -2
- package/dist/types/session/claude-session-store.d.ts +13 -0
- package/dist/types/session/codex-session-store.d.ts +14 -0
- package/dist/types/session/credential-pin.d.ts +58 -0
- package/dist/types/session/foreign-session-import.d.ts +15 -0
- package/dist/types/session/foreign-session-jsonl.d.ts +9 -0
- package/dist/types/session/foreign-session-store.d.ts +23 -0
- package/dist/types/session/session-advisors.d.ts +47 -2
- package/dist/types/session/session-entries.d.ts +19 -1
- package/dist/types/session/session-handoff.d.ts +4 -1
- package/dist/types/session/session-loader.d.ts +2 -0
- package/dist/types/session/session-maintenance.d.ts +1 -1
- package/dist/types/session/session-manager.d.ts +21 -0
- package/dist/types/session/session-tools.d.ts +25 -1
- package/dist/types/session/turn-recovery.d.ts +10 -4
- package/dist/types/slash-commands/helpers/security.d.ts +2 -0
- package/dist/types/system-prompt.d.ts +4 -1
- package/dist/types/task/executor.d.ts +14 -2
- package/dist/types/task/render.d.ts +7 -0
- package/dist/types/thinking.d.ts +18 -6
- package/dist/types/tools/acp-bridge.d.ts +28 -5
- package/dist/types/tools/builtin-names.d.ts +1 -1
- package/dist/types/tools/grep.d.ts +17 -1
- package/dist/types/tools/hub/launch.d.ts +5 -1
- package/dist/types/tools/index.d.ts +10 -2
- package/dist/types/tools/path-utils.d.ts +19 -0
- package/dist/types/tools/read.d.ts +2 -0
- package/dist/types/tools/security-scan.d.ts +96 -0
- package/dist/types/tools/xdev.d.ts +2 -1
- package/dist/types/utils/changelog.d.ts +19 -0
- package/dist/types/utils/git.d.ts +10 -0
- package/dist/types/utils/jj.d.ts +11 -4
- package/dist/types/web/search/types.d.ts +2 -2
- package/package.json +12 -12
- package/scripts/security-compare.ts +40 -0
- package/src/advisor/runtime.ts +138 -12
- package/src/advisor/transcript-recorder.ts +56 -0
- package/src/auto-thinking/classifier.ts +48 -10
- package/src/cli/args.ts +6 -0
- package/src/cli/flag-tables.ts +2 -0
- package/src/cli/gallery-fixtures/fs.ts +25 -0
- package/src/cli/session-picker.ts +34 -17
- package/src/cli/ttsr-cli.ts +19 -1
- package/src/cli.ts +9 -0
- package/src/collab/guest.ts +37 -4
- package/src/collab/host.ts +1 -0
- package/src/commands/launch.ts +6 -0
- package/src/config/model-discovery.ts +32 -5
- package/src/config/model-registry.ts +107 -25
- package/src/config/settings-schema.ts +77 -6
- package/src/config/settings.ts +25 -0
- package/src/cursor-bridge-tools.ts +81 -0
- package/src/cursor.ts +463 -12
- package/src/edit/edit-clipboard.ts +23 -0
- package/src/edit/hashline/diff.ts +49 -10
- package/src/edit/hashline/execute.ts +38 -6
- package/src/edit/hashline/filesystem.ts +27 -3
- package/src/edit/index.ts +12 -2
- package/src/edit/renderer.ts +4 -4
- package/src/edit/streaming.ts +17 -2
- package/src/eval/py/prelude.py +7 -3
- package/src/export/html/template.js +1 -1
- package/src/extensibility/extensions/runner.ts +70 -0
- package/src/extensibility/extensions/types.ts +30 -0
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +65 -13
- package/src/internal-urls/index.ts +1 -0
- package/src/internal-urls/local-protocol.ts +3 -2
- package/src/internal-urls/mcp-protocol.ts +4 -1
- package/src/internal-urls/router.ts +4 -1
- package/src/internal-urls/security-protocol.ts +261 -0
- package/src/internal-urls/types.ts +1 -1
- package/src/launch/terminal-output-worker-client.ts +53 -0
- package/src/launch/terminal-output-worker-protocol.ts +11 -0
- package/src/launch/terminal-output-worker.ts +23 -0
- package/src/lsp/client.ts +18 -3
- package/src/lsp/index.ts +7 -0
- package/src/lsp/render.ts +1 -1
- package/src/main.ts +127 -56
- package/src/mcp/manager.ts +120 -18
- package/src/modes/acp/acp-agent.ts +15 -9
- package/src/modes/components/chat-transcript-builder.ts +30 -11
- package/src/modes/components/codex-reset-fireworks.ts +369 -0
- package/src/modes/components/read-tool-group.ts +159 -17
- package/src/modes/components/session-selector.ts +19 -3
- package/src/modes/components/status-line/component.ts +381 -54
- package/src/modes/components/tool-execution.ts +78 -31
- package/src/modes/components/usage-row.ts +10 -5
- package/src/modes/controllers/event-controller.ts +26 -4
- package/src/modes/controllers/mcp-command-controller.ts +68 -3
- package/src/modes/controllers/selector-controller.ts +94 -36
- package/src/modes/controllers/tan-command-controller.ts +14 -0
- package/src/modes/interactive-mode.ts +31 -17
- package/src/modes/rpc/host-uris.ts +6 -0
- package/src/modes/rpc/rpc-client.ts +18 -0
- package/src/modes/rpc/rpc-mode.ts +16 -1
- package/src/modes/rpc/rpc-types.ts +11 -0
- package/src/modes/theme/theme.ts +151 -6
- package/src/modes/types.ts +2 -1
- package/src/modes/utils/ui-helpers.ts +38 -21
- package/src/prompts/agents/security-reviewer.md +75 -0
- package/src/prompts/security/scan-coordinator.md +7 -0
- package/src/prompts/security/scan-request.md +21 -0
- package/src/prompts/security/validate-request.md +8 -0
- package/src/prompts/system/auto-thinking-difficulty.md +4 -2
- package/src/prompts/system/plan-mode-active.md +2 -2
- package/src/prompts/system/system-prompt.md +6 -0
- package/src/prompts/system/xdev-mount-notice.md +1 -1
- package/src/prompts/tools/bash.md +15 -17
- package/src/prompts/tools/checkpoint.md +1 -1
- package/src/prompts/tools/glob.md +7 -6
- package/src/prompts/tools/grep.md +7 -6
- package/src/prompts/tools/security-publish.md +1 -0
- package/src/prompts/tools/security-scan.md +1 -0
- package/src/sdk.ts +212 -21
- package/src/security/auth.ts +98 -0
- package/src/security/cloud.ts +686 -0
- package/src/security/comparison.ts +255 -0
- package/src/security/contracts/ids.ts +111 -0
- package/src/security/contracts/index.ts +4 -0
- package/src/security/contracts/schemas.ts +201 -0
- package/src/security/contracts/types.ts +254 -0
- package/src/security/contracts/validation.ts +65 -0
- package/src/security/coordinator.ts +708 -0
- package/src/security/importers/codex-security.ts +387 -0
- package/src/security/importers/index.ts +2 -0
- package/src/security/importers/sarif.ts +357 -0
- package/src/security/index.ts +13 -0
- package/src/security/preflight.ts +405 -0
- package/src/security/provenance.ts +106 -0
- package/src/security/publication.ts +326 -0
- package/src/security/remediation.ts +93 -0
- package/src/security/resource-output.ts +50 -0
- package/src/security/sarif.ts +78 -0
- package/src/security/store.ts +430 -0
- package/src/session/agent-session-events.ts +1 -0
- package/src/session/agent-session-types.ts +40 -1
- package/src/session/agent-session.ts +302 -175
- package/src/session/claude-session-store.ts +426 -0
- package/src/session/codex-session-store.ts +673 -0
- package/src/session/credential-pin.ts +93 -0
- package/src/session/foreign-session-import.ts +52 -0
- package/src/session/foreign-session-jsonl.ts +29 -0
- package/src/session/foreign-session-store.ts +26 -0
- package/src/session/model-controls.ts +11 -3
- package/src/session/session-advisors.ts +140 -16
- package/src/session/session-entries.ts +21 -1
- package/src/session/session-handoff.ts +20 -3
- package/src/session/session-loader.ts +25 -9
- package/src/session/session-maintenance.ts +59 -11
- package/src/session/session-manager.ts +61 -0
- package/src/session/session-tools.ts +107 -5
- package/src/session/settings-stream-fn.ts +1 -0
- package/src/session/turn-recovery.ts +97 -51
- package/src/slash-commands/builtin-registry.ts +152 -3
- package/src/slash-commands/helpers/security.ts +451 -0
- package/src/system-prompt.ts +7 -2
- package/src/task/agents.ts +2 -0
- package/src/task/executor.ts +20 -4
- package/src/task/render.ts +20 -4
- package/src/task/structured-subagent.ts +6 -4
- package/src/thinking.ts +29 -10
- package/src/tools/acp-bridge.ts +52 -8
- package/src/tools/browser.ts +5 -0
- package/src/tools/builtin-names.ts +1 -0
- package/src/tools/checkpoint.ts +0 -13
- package/src/tools/grep.ts +64 -8
- package/src/tools/hub/launch.ts +10 -15
- package/src/tools/index.ts +37 -5
- package/src/tools/path-utils.ts +91 -0
- package/src/tools/read.ts +3 -0
- package/src/tools/security-scan.ts +287 -0
- package/src/tools/write.ts +9 -3
- package/src/tools/xdev.ts +39 -14
- package/src/utils/changelog.ts +144 -4
- package/src/utils/git.ts +61 -14
- package/src/utils/jj.ts +19 -13
- package/src/web/search/providers/codex.ts +34 -0
- package/src/web/search/types.ts +1 -1
|
@@ -0,0 +1,686 @@
|
|
|
1
|
+
import { decodeJwt } from "@oh-my-pi/pi-ai/registry/oauth/openai-codex";
|
|
2
|
+
import type { AuthStorage } from "../session/auth-storage";
|
|
3
|
+
import * as git from "../utils/git";
|
|
4
|
+
import { resolveExactSecurityOAuthAccess } from "./auth";
|
|
5
|
+
import {
|
|
6
|
+
createSecurityEvidenceId,
|
|
7
|
+
createSecurityFindingFingerprint,
|
|
8
|
+
createSecurityFindingId,
|
|
9
|
+
createSecurityOccurrenceId,
|
|
10
|
+
createSecurityScanId,
|
|
11
|
+
type SecurityAccountRef,
|
|
12
|
+
type SecurityConfidenceLevel,
|
|
13
|
+
type SecurityDispositionStatus,
|
|
14
|
+
type SecurityEvidence,
|
|
15
|
+
type SecurityFinding,
|
|
16
|
+
type SecurityLocation,
|
|
17
|
+
type SecurityProducer,
|
|
18
|
+
type SecurityProvenance,
|
|
19
|
+
type SecurityScanBundle,
|
|
20
|
+
type SecuritySeverityLevel,
|
|
21
|
+
} from "./contracts";
|
|
22
|
+
import { exportSecurityBundleToSarif } from "./sarif";
|
|
23
|
+
import type { SecurityStore } from "./store";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* ChatGPT's Codex Security cloud control plane. This authenticated web-app
|
|
27
|
+
* contract is not a public OpenAI API: keep it isolated here, fail closed on
|
|
28
|
+
* shape changes, and never use it as a fallback for OMP-native inference.
|
|
29
|
+
*/
|
|
30
|
+
const DEFAULT_CLOUD_BASE_URL = "https://chatgpt.com/backend-api/aardvark";
|
|
31
|
+
const ALL_FINDING_STATUSES = ["new", "triaged", "in_progress", "fixed", "wontfix", "duplicate", "false_positive"];
|
|
32
|
+
const CLOUD_PRODUCER: SecurityProducer = {
|
|
33
|
+
kind: "codex-security-cloud",
|
|
34
|
+
name: "Codex Security cloud",
|
|
35
|
+
vendor: "OpenAI",
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
type JsonObject = Record<string, unknown>;
|
|
39
|
+
|
|
40
|
+
export type CodexSecurityCloudFetch = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
41
|
+
|
|
42
|
+
export interface CodexSecurityCloudClientOptions {
|
|
43
|
+
authStorage: AuthStorage;
|
|
44
|
+
account: SecurityAccountRef;
|
|
45
|
+
baseUrl?: string;
|
|
46
|
+
fetch?: CodexSecurityCloudFetch;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface CodexSecurityCloudConfiguration {
|
|
50
|
+
id: string;
|
|
51
|
+
sourceId?: string;
|
|
52
|
+
repositoryId: string;
|
|
53
|
+
repositoryUrl: string;
|
|
54
|
+
environmentId: string;
|
|
55
|
+
state?: string;
|
|
56
|
+
currentStep?: string;
|
|
57
|
+
scanType?: string;
|
|
58
|
+
remainingScans?: number;
|
|
59
|
+
totalScans?: number;
|
|
60
|
+
createdAt?: string;
|
|
61
|
+
updatedAt?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface CodexSecurityCloudConfigurationPage {
|
|
65
|
+
items: CodexSecurityCloudConfiguration[];
|
|
66
|
+
nextCursor?: string;
|
|
67
|
+
totalInAccount?: number;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface StartCodexSecurityCloudScanInput {
|
|
71
|
+
repositoryId: string;
|
|
72
|
+
repositoryUrl: string;
|
|
73
|
+
environmentId: string;
|
|
74
|
+
lookbackDays?: number | "all";
|
|
75
|
+
maintainerAttackConcerns?: string;
|
|
76
|
+
maintainerFocusAreas?: string;
|
|
77
|
+
maintainerAdditionalContext?: string;
|
|
78
|
+
signal?: AbortSignal;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface CodexSecurityCloudStats {
|
|
82
|
+
configurationId: string;
|
|
83
|
+
sourceConfigurationId?: string;
|
|
84
|
+
currentStep?: string;
|
|
85
|
+
pendingCommits: number;
|
|
86
|
+
finishedCommits: number;
|
|
87
|
+
failedCommits: number;
|
|
88
|
+
findingCounts: Record<SecuritySeverityLevel, number>;
|
|
89
|
+
lastScannedCommit?: string;
|
|
90
|
+
lastScannedAt?: string;
|
|
91
|
+
updatedAt?: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface PullCodexSecurityCloudResultsInput {
|
|
95
|
+
client: CodexSecurityCloudClient;
|
|
96
|
+
configurationId: string;
|
|
97
|
+
store: SecurityStore;
|
|
98
|
+
signal?: AbortSignal;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function object(value: unknown): JsonObject {
|
|
102
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
103
|
+
throw new Error("Codex Security cloud returned an invalid object");
|
|
104
|
+
return value as JsonObject;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function optionalObject(value: unknown): JsonObject {
|
|
108
|
+
return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonObject) : {};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function requiredString(value: unknown, field: string): string {
|
|
112
|
+
if (typeof value !== "string" || value.length === 0)
|
|
113
|
+
throw new Error(`Codex Security cloud response is missing ${field}`);
|
|
114
|
+
return value;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function optionalString(value: unknown): string | undefined {
|
|
118
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function finiteNumber(value: unknown, fallback = 0): number {
|
|
122
|
+
return typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function positiveInteger(value: unknown): number | undefined {
|
|
126
|
+
return typeof value === "number" && Number.isInteger(value) && value >= 1 ? value : undefined;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function normalizeConfiguration(value: unknown): CodexSecurityCloudConfiguration {
|
|
130
|
+
const raw = object(value);
|
|
131
|
+
const scanInput = object(raw.scan_input);
|
|
132
|
+
const id = requiredString(raw.hid ?? raw.id, "configuration id");
|
|
133
|
+
const configuration: CodexSecurityCloudConfiguration = {
|
|
134
|
+
id,
|
|
135
|
+
repositoryId: requiredString(scanInput.repo_id, "repository id"),
|
|
136
|
+
repositoryUrl: requiredString(scanInput.repo_url, "repository URL"),
|
|
137
|
+
environmentId: requiredString(scanInput.environment_id, "environment id"),
|
|
138
|
+
};
|
|
139
|
+
const sourceId = optionalString(raw.id);
|
|
140
|
+
if (sourceId && sourceId !== id) configuration.sourceId = sourceId;
|
|
141
|
+
const state = optionalString(scanInput.state);
|
|
142
|
+
if (state) configuration.state = state;
|
|
143
|
+
const currentStep = optionalString(raw.current_step);
|
|
144
|
+
if (currentStep) configuration.currentStep = currentStep;
|
|
145
|
+
const scanType = optionalString(scanInput.scan_type);
|
|
146
|
+
if (scanType) configuration.scanType = scanType;
|
|
147
|
+
const remainingScans = typeof raw.scans_remaining === "number" ? raw.scans_remaining : raw.remaining_scans;
|
|
148
|
+
if (typeof remainingScans === "number" && Number.isFinite(remainingScans))
|
|
149
|
+
configuration.remainingScans = remainingScans;
|
|
150
|
+
if (typeof raw.total_scans === "number" && Number.isFinite(raw.total_scans))
|
|
151
|
+
configuration.totalScans = raw.total_scans;
|
|
152
|
+
const createdAt = optionalString(raw.created_at);
|
|
153
|
+
if (createdAt) configuration.createdAt = createdAt;
|
|
154
|
+
const updatedAt = optionalString(raw.updated_at);
|
|
155
|
+
if (updatedAt) configuration.updatedAt = updatedAt;
|
|
156
|
+
return configuration;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function jwtSubject(accessToken: string): string {
|
|
160
|
+
const claims = decodeJwt(accessToken);
|
|
161
|
+
if (!claims) throw new Error("The selected ChatGPT credential is not a valid JWT");
|
|
162
|
+
return requiredString(claims.sub ?? claims.user_id, "authenticated user id");
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export class CodexSecurityCloudHttpError extends Error {
|
|
166
|
+
constructor(
|
|
167
|
+
readonly status: number,
|
|
168
|
+
readonly endpoint: string,
|
|
169
|
+
) {
|
|
170
|
+
super(`Codex Security cloud request failed (${status}) at ${endpoint}`);
|
|
171
|
+
this.name = "CodexSecurityCloudHttpError";
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
interface CloudRequestOptions {
|
|
176
|
+
method?: "GET" | "POST";
|
|
177
|
+
query?: Record<string, string | number | undefined>;
|
|
178
|
+
body?: JsonObject | ((accessToken: string) => JsonObject);
|
|
179
|
+
signal?: AbortSignal;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export class CodexSecurityCloudClient {
|
|
183
|
+
readonly #authStorage: AuthStorage;
|
|
184
|
+
readonly #account: SecurityAccountRef;
|
|
185
|
+
readonly #baseUrl: string;
|
|
186
|
+
readonly #fetch: CodexSecurityCloudFetch;
|
|
187
|
+
|
|
188
|
+
constructor(options: CodexSecurityCloudClientOptions) {
|
|
189
|
+
if (options.account.provider !== "openai-codex") {
|
|
190
|
+
throw new Error("Codex Security cloud requires an openai-codex ChatGPT OAuth credential");
|
|
191
|
+
}
|
|
192
|
+
this.#authStorage = options.authStorage;
|
|
193
|
+
this.#account = options.account;
|
|
194
|
+
this.#baseUrl = (options.baseUrl ?? DEFAULT_CLOUD_BASE_URL).replace(/\/$/, "");
|
|
195
|
+
this.#fetch = options.fetch ?? fetch;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async #request(pathname: string, options: CloudRequestOptions = {}): Promise<JsonObject> {
|
|
199
|
+
const url = new URL(`${this.#baseUrl}/${pathname.replace(/^\//, "")}`);
|
|
200
|
+
for (const [key, value] of Object.entries(options.query ?? {})) {
|
|
201
|
+
if (value !== undefined) url.searchParams.set(key, String(value));
|
|
202
|
+
}
|
|
203
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
204
|
+
const access = await resolveExactSecurityOAuthAccess(this.#authStorage, this.#account, {
|
|
205
|
+
forceRefresh: attempt > 0,
|
|
206
|
+
signal: options.signal,
|
|
207
|
+
});
|
|
208
|
+
const body = typeof options.body === "function" ? options.body(access.accessToken) : options.body;
|
|
209
|
+
const headers: Record<string, string> = {
|
|
210
|
+
Accept: "application/json",
|
|
211
|
+
Authorization: `Bearer ${access.accessToken}`,
|
|
212
|
+
};
|
|
213
|
+
const accountId = access.accountId ?? this.#account.accountId;
|
|
214
|
+
if (accountId) headers["ChatGPT-Account-Id"] = accountId;
|
|
215
|
+
if (body) headers["Content-Type"] = "application/json";
|
|
216
|
+
const response = await this.#fetch(url, {
|
|
217
|
+
method: options.method ?? "GET",
|
|
218
|
+
headers,
|
|
219
|
+
body: body ? JSON.stringify(body) : undefined,
|
|
220
|
+
signal: options.signal,
|
|
221
|
+
});
|
|
222
|
+
if (response.status === 401 && attempt === 0) continue;
|
|
223
|
+
if (!response.ok) throw new CodexSecurityCloudHttpError(response.status, url.pathname);
|
|
224
|
+
return object(await response.json());
|
|
225
|
+
}
|
|
226
|
+
throw new Error("Codex Security cloud authentication refresh failed");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async listConfigurations(
|
|
230
|
+
options: { limit?: number; cursor?: string; signal?: AbortSignal } = {},
|
|
231
|
+
): Promise<CodexSecurityCloudConfigurationPage> {
|
|
232
|
+
const raw = await this.#request("scan_configurations", {
|
|
233
|
+
query: { limit: options.limit ?? 100, cursor: options.cursor },
|
|
234
|
+
signal: options.signal,
|
|
235
|
+
});
|
|
236
|
+
const items = Array.isArray(raw.items) ? raw.items.map(normalizeConfiguration) : [];
|
|
237
|
+
const result: CodexSecurityCloudConfigurationPage = { items };
|
|
238
|
+
const nextCursor = optionalString(raw.next_cursor);
|
|
239
|
+
if (nextCursor) result.nextCursor = nextCursor;
|
|
240
|
+
if (typeof raw.total_in_account === "number") result.totalInAccount = raw.total_in_account;
|
|
241
|
+
return result;
|
|
242
|
+
}
|
|
243
|
+
async listAllConfigurations(signal?: AbortSignal): Promise<CodexSecurityCloudConfiguration[]> {
|
|
244
|
+
const configurations: CodexSecurityCloudConfiguration[] = [];
|
|
245
|
+
let cursor: string | undefined;
|
|
246
|
+
do {
|
|
247
|
+
const page = await this.listConfigurations({ limit: 500, cursor, signal });
|
|
248
|
+
configurations.push(...page.items);
|
|
249
|
+
cursor = page.nextCursor;
|
|
250
|
+
} while (cursor);
|
|
251
|
+
return configurations;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
async getConfiguration(configurationId: string, signal?: AbortSignal): Promise<CodexSecurityCloudConfiguration> {
|
|
255
|
+
let cursor: string | undefined;
|
|
256
|
+
do {
|
|
257
|
+
const page = await this.listConfigurations({ limit: 500, cursor, signal });
|
|
258
|
+
const found = page.items.find(item => item.id === configurationId || item.sourceId === configurationId);
|
|
259
|
+
if (found) return found;
|
|
260
|
+
cursor = page.nextCursor;
|
|
261
|
+
} while (cursor);
|
|
262
|
+
throw new Error(`Unknown Codex Security cloud configuration: ${configurationId}`);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async startScan(input: StartCodexSecurityCloudScanInput): Promise<CodexSecurityCloudConfiguration> {
|
|
266
|
+
if (
|
|
267
|
+
input.lookbackDays !== undefined &&
|
|
268
|
+
input.lookbackDays !== "all" &&
|
|
269
|
+
(!Number.isInteger(input.lookbackDays) || input.lookbackDays < 1)
|
|
270
|
+
) {
|
|
271
|
+
throw new Error("lookbackDays must be a positive integer or 'all'");
|
|
272
|
+
}
|
|
273
|
+
const raw = await this.#request("scan_configurations", {
|
|
274
|
+
method: "POST",
|
|
275
|
+
signal: input.signal,
|
|
276
|
+
body: accessToken => {
|
|
277
|
+
const scanInput: JsonObject = {
|
|
278
|
+
environment_id: input.environmentId,
|
|
279
|
+
lookback_days: input.lookbackDays === "all" ? null : (input.lookbackDays ?? 30),
|
|
280
|
+
notification_rules: [],
|
|
281
|
+
owner_id: jwtSubject(accessToken),
|
|
282
|
+
repo_id: input.repositoryId,
|
|
283
|
+
repo_url: input.repositoryUrl,
|
|
284
|
+
share_targets: [],
|
|
285
|
+
state: "enabled",
|
|
286
|
+
};
|
|
287
|
+
if (input.maintainerAttackConcerns) scanInput.maintainer_attack_concerns = input.maintainerAttackConcerns;
|
|
288
|
+
if (input.maintainerFocusAreas) scanInput.maintainer_focus_areas = input.maintainerFocusAreas;
|
|
289
|
+
if (input.maintainerAdditionalContext)
|
|
290
|
+
scanInput.maintainer_additional_context = input.maintainerAdditionalContext;
|
|
291
|
+
return { scan_input: scanInput };
|
|
292
|
+
},
|
|
293
|
+
});
|
|
294
|
+
return normalizeConfiguration(raw);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async getStats(configurationId: string, signal?: AbortSignal): Promise<CodexSecurityCloudStats> {
|
|
298
|
+
const raw = await this.#request(`scan_configurations/${encodeURIComponent(configurationId)}/stats`, { signal });
|
|
299
|
+
const result: CodexSecurityCloudStats = {
|
|
300
|
+
configurationId,
|
|
301
|
+
pendingCommits: finiteNumber(raw.pending_commits),
|
|
302
|
+
finishedCommits: finiteNumber(raw.finished_commits),
|
|
303
|
+
failedCommits: finiteNumber(raw.failed_commits),
|
|
304
|
+
findingCounts: {
|
|
305
|
+
critical: finiteNumber(raw.critical_findings),
|
|
306
|
+
high: finiteNumber(raw.high_findings),
|
|
307
|
+
medium: finiteNumber(raw.medium_findings),
|
|
308
|
+
low: finiteNumber(raw.low_findings),
|
|
309
|
+
informational: finiteNumber(raw.informational_findings),
|
|
310
|
+
},
|
|
311
|
+
};
|
|
312
|
+
const sourceConfigurationId = optionalString(raw.config_id);
|
|
313
|
+
if (sourceConfigurationId && sourceConfigurationId !== configurationId) {
|
|
314
|
+
result.sourceConfigurationId = sourceConfigurationId;
|
|
315
|
+
}
|
|
316
|
+
const currentStep = optionalString(raw.current_step);
|
|
317
|
+
if (currentStep) result.currentStep = currentStep;
|
|
318
|
+
const lastScannedCommit = optionalString(raw.last_scanned_commit_hash);
|
|
319
|
+
if (lastScannedCommit) result.lastScannedCommit = lastScannedCommit;
|
|
320
|
+
const lastScannedAt = optionalString(raw.last_scanned_commit_dt);
|
|
321
|
+
if (lastScannedAt) result.lastScannedAt = lastScannedAt;
|
|
322
|
+
const updatedAt = optionalString(raw.updated_at);
|
|
323
|
+
if (updatedAt) result.updatedAt = updatedAt;
|
|
324
|
+
return result;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async listFindingDetails(
|
|
328
|
+
repositoryUrl: string,
|
|
329
|
+
configuration: CodexSecurityCloudConfiguration,
|
|
330
|
+
signal?: AbortSignal,
|
|
331
|
+
): Promise<JsonObject[]> {
|
|
332
|
+
const summaries: JsonObject[] = [];
|
|
333
|
+
let cursor: string | undefined;
|
|
334
|
+
do {
|
|
335
|
+
const page = await this.#request("scan-findings", {
|
|
336
|
+
query: {
|
|
337
|
+
repo: repositoryUrl,
|
|
338
|
+
limit: 500,
|
|
339
|
+
cursor,
|
|
340
|
+
status: ALL_FINDING_STATUSES.join(","),
|
|
341
|
+
},
|
|
342
|
+
signal,
|
|
343
|
+
});
|
|
344
|
+
if (Array.isArray(page.items)) summaries.push(...page.items.map(object));
|
|
345
|
+
cursor = optionalString(page.next_cursor);
|
|
346
|
+
} while (cursor);
|
|
347
|
+
const configurationIds = new Set([configuration.id, configuration.sourceId].filter((id): id is string => !!id));
|
|
348
|
+
const attributed = (item: JsonObject): boolean => {
|
|
349
|
+
const configuredScanId = optionalString(item.configured_scan_id);
|
|
350
|
+
return configuredScanId === undefined || configurationIds.has(configuredScanId);
|
|
351
|
+
};
|
|
352
|
+
const selected = summaries.filter(attributed);
|
|
353
|
+
const details: JsonObject[] = [];
|
|
354
|
+
for (let index = 0; index < selected.length; index += 8) {
|
|
355
|
+
const batch = selected.slice(index, index + 8);
|
|
356
|
+
details.push(
|
|
357
|
+
...(await Promise.all(
|
|
358
|
+
batch.map(item => {
|
|
359
|
+
const id = requiredString(item.hid ?? item.id, "finding id");
|
|
360
|
+
return this.#request(`scan-findings/${encodeURIComponent(id)}`, { signal });
|
|
361
|
+
}),
|
|
362
|
+
)),
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
// The list request is scoped only by repository URL; a summary may omit its
|
|
366
|
+
// configuration id, so re-verify attribution on each detail response before import.
|
|
367
|
+
return details.filter(attributed);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function normalizePath(value: unknown): string | undefined {
|
|
372
|
+
if (typeof value !== "string") return undefined;
|
|
373
|
+
const normalized = value.trim().replaceAll("\\", "/").replace(/^\.\//, "");
|
|
374
|
+
if (
|
|
375
|
+
!normalized ||
|
|
376
|
+
normalized.startsWith("/") ||
|
|
377
|
+
/^[a-zA-Z]:\//.test(normalized) ||
|
|
378
|
+
normalized.split("/").includes("..")
|
|
379
|
+
)
|
|
380
|
+
return undefined;
|
|
381
|
+
return normalized;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function severity(value: unknown): SecuritySeverityLevel {
|
|
385
|
+
return value === "critical" || value === "high" || value === "medium" || value === "low" || value === "informational"
|
|
386
|
+
? value
|
|
387
|
+
: "informational";
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function confidence(commit: JsonObject): SecurityConfidenceLevel {
|
|
391
|
+
const value = commit.validation_confidence;
|
|
392
|
+
if (typeof value === "number") return value >= 0.67 ? "high" : value >= 0.34 ? "medium" : "low";
|
|
393
|
+
return commit.validated === true ? "high" : "medium";
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
function disposition(value: unknown): SecurityDispositionStatus {
|
|
397
|
+
switch (value) {
|
|
398
|
+
case "fixed":
|
|
399
|
+
return "fixed";
|
|
400
|
+
case "false_positive":
|
|
401
|
+
return "false_positive";
|
|
402
|
+
case "wontfix":
|
|
403
|
+
return "wont_fix";
|
|
404
|
+
case "duplicate":
|
|
405
|
+
return "accepted_risk";
|
|
406
|
+
default:
|
|
407
|
+
return "open";
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
function text(value: unknown): string | undefined {
|
|
412
|
+
if (typeof value === "string" && value.length > 0) return value;
|
|
413
|
+
return undefined;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function locationsAndEvidence(
|
|
417
|
+
commit: JsonObject,
|
|
418
|
+
fingerprintSeed: string,
|
|
419
|
+
): { locations: SecurityLocation[]; evidence: SecurityEvidence[] } {
|
|
420
|
+
const relevantLines = Array.isArray(commit.relevant_lines) ? commit.relevant_lines : [];
|
|
421
|
+
const locations: SecurityLocation[] = [];
|
|
422
|
+
const evidenceInputs: Array<{
|
|
423
|
+
label: string;
|
|
424
|
+
explanation: string;
|
|
425
|
+
excerpt?: string;
|
|
426
|
+
location?: SecurityLocation;
|
|
427
|
+
kind: SecurityEvidence["kind"];
|
|
428
|
+
}> = [];
|
|
429
|
+
for (const [index, value] of relevantLines.entries()) {
|
|
430
|
+
const line = optionalObject(value);
|
|
431
|
+
const sourcePath = normalizePath(line.path);
|
|
432
|
+
const startLine = positiveInteger(line.start_line_number);
|
|
433
|
+
if (!sourcePath || !startLine) continue;
|
|
434
|
+
const location: SecurityLocation = { path: sourcePath, startLine };
|
|
435
|
+
const endLine = positiveInteger(line.end_line_number);
|
|
436
|
+
if (endLine && endLine >= startLine) location.endLine = endLine;
|
|
437
|
+
locations.push(location);
|
|
438
|
+
const entry: (typeof evidenceInputs)[number] = {
|
|
439
|
+
kind: "code",
|
|
440
|
+
label: `Cloud source evidence ${index + 1}`,
|
|
441
|
+
explanation: text(line.comment) ?? "Source location reported by Codex Security cloud.",
|
|
442
|
+
location,
|
|
443
|
+
};
|
|
444
|
+
const excerpt = text(line.content);
|
|
445
|
+
if (excerpt) entry.excerpt = excerpt;
|
|
446
|
+
evidenceInputs.push(entry);
|
|
447
|
+
}
|
|
448
|
+
if (locations.length === 0 && Array.isArray(commit.files_involved)) {
|
|
449
|
+
for (const value of commit.files_involved) {
|
|
450
|
+
const sourcePath = normalizePath(value);
|
|
451
|
+
if (sourcePath) locations.push({ path: sourcePath, startLine: 1, role: "cloud-file" });
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if (locations.length === 0)
|
|
455
|
+
throw new Error("Codex Security cloud finding has no usable repository-relative location");
|
|
456
|
+
const validationReport = text(commit.validation_report) ?? text(commit.fix_check_report);
|
|
457
|
+
if (validationReport) {
|
|
458
|
+
evidenceInputs.push({
|
|
459
|
+
kind: "validation",
|
|
460
|
+
label: "Cloud validation",
|
|
461
|
+
explanation: validationReport,
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
const evidence = evidenceInputs.map((item, index) => ({
|
|
465
|
+
id: createSecurityEvidenceId(fingerprintSeed, item.label, index),
|
|
466
|
+
...item,
|
|
467
|
+
}));
|
|
468
|
+
return { locations, evidence };
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function normalizeFinding(
|
|
472
|
+
raw: JsonObject,
|
|
473
|
+
scanId: string,
|
|
474
|
+
configuration: CodexSecurityCloudConfiguration,
|
|
475
|
+
importedAt: string,
|
|
476
|
+
): SecurityFinding {
|
|
477
|
+
const commit = optionalObject(raw.commit_analysis);
|
|
478
|
+
const title = requiredString(commit.title ?? raw.title, "finding title");
|
|
479
|
+
const ruleId =
|
|
480
|
+
optionalString(commit.rule_id) ??
|
|
481
|
+
`codex-security:${Bun.SHA256.hash(title.trim().toLowerCase(), "hex").slice(0, 16)}`;
|
|
482
|
+
const category = optionalString(commit.category) ?? "codex-security";
|
|
483
|
+
const cloudId = requiredString(raw.hid ?? raw.id, "finding id");
|
|
484
|
+
const preliminary = locationsAndEvidence(commit, cloudId);
|
|
485
|
+
const fingerprint = createSecurityFindingFingerprint({ ruleId, category, locations: preliminary.locations });
|
|
486
|
+
const evidence = preliminary.evidence.map((item, index) => ({
|
|
487
|
+
...item,
|
|
488
|
+
id: createSecurityEvidenceId(fingerprint, item.label, index),
|
|
489
|
+
}));
|
|
490
|
+
const validationEvidenceIds = evidence.filter(item => item.kind === "validation").map(item => item.id);
|
|
491
|
+
const createdAt = optionalString(raw.created_at) ?? importedAt;
|
|
492
|
+
const producer = { ...CLOUD_PRODUCER };
|
|
493
|
+
const sourceIds: Record<string, string> = { cloudConfigurationId: configuration.id, cloudFindingId: cloudId };
|
|
494
|
+
for (const [key, value] of [
|
|
495
|
+
["cloudSourceFindingId", raw.id],
|
|
496
|
+
["cloudScanId", raw.scan_id],
|
|
497
|
+
["cloudJobId", raw.job_id],
|
|
498
|
+
] as const) {
|
|
499
|
+
if (typeof value === "string" && value.length > 0) sourceIds[key] = value;
|
|
500
|
+
}
|
|
501
|
+
const upstream: NonNullable<SecurityProvenance["upstream"]> = { repository: configuration.repositoryUrl };
|
|
502
|
+
const revision = optionalString(commit.commit_hash);
|
|
503
|
+
if (revision) upstream.revision = revision;
|
|
504
|
+
const provenance: SecurityProvenance = {
|
|
505
|
+
producer,
|
|
506
|
+
createdAt,
|
|
507
|
+
importedAt,
|
|
508
|
+
sourceIds,
|
|
509
|
+
upstream,
|
|
510
|
+
metadata: {
|
|
511
|
+
cloudStatus: raw.status ?? null,
|
|
512
|
+
bugStatus: commit.bug_status ?? null,
|
|
513
|
+
securityRelated: commit.security_related ?? null,
|
|
514
|
+
validationMethod: commit.validation_method ?? null,
|
|
515
|
+
},
|
|
516
|
+
};
|
|
517
|
+
const findingSeverity: SecurityFinding["severity"] = { level: severity(raw.criticality ?? commit.criticality) };
|
|
518
|
+
const severityRationale = text(raw.criticality_reason);
|
|
519
|
+
if (severityRationale) findingSeverity.rationale = severityRationale;
|
|
520
|
+
const validation: SecurityFinding["validation"] = {
|
|
521
|
+
status: commit.validated === true ? "validated" : "unvalidated",
|
|
522
|
+
evidenceIds: validationEvidenceIds,
|
|
523
|
+
};
|
|
524
|
+
const validatedAt = optionalString(commit.validation_finished_at);
|
|
525
|
+
if (validatedAt) validation.validatedAt = validatedAt;
|
|
526
|
+
const validationSummary = text(commit.validation_report);
|
|
527
|
+
if (validationSummary) validation.summary = validationSummary;
|
|
528
|
+
const findingDisposition: SecurityFinding["disposition"] = { status: disposition(raw.status) };
|
|
529
|
+
const dispositionRationale = text(raw.resolution_reason);
|
|
530
|
+
if (dispositionRationale) findingDisposition.rationale = dispositionRationale;
|
|
531
|
+
const dispositionUpdatedAt = optionalString(raw.updated_at);
|
|
532
|
+
if (dispositionUpdatedAt) findingDisposition.updatedAt = dispositionUpdatedAt;
|
|
533
|
+
const finding: SecurityFinding = {
|
|
534
|
+
id: createSecurityFindingId(fingerprint),
|
|
535
|
+
scanId,
|
|
536
|
+
fingerprint,
|
|
537
|
+
ruleId,
|
|
538
|
+
title,
|
|
539
|
+
summary: text(commit.description) ?? text(raw.description) ?? title,
|
|
540
|
+
severity: findingSeverity,
|
|
541
|
+
confidence: { level: confidence(commit) },
|
|
542
|
+
taxonomy: { category, cwe: [] },
|
|
543
|
+
occurrences: [
|
|
544
|
+
{
|
|
545
|
+
id: createSecurityOccurrenceId(fingerprint, preliminary.locations),
|
|
546
|
+
locations: preliminary.locations,
|
|
547
|
+
evidenceIds: evidence.filter(item => item.kind === "code").map(item => item.id),
|
|
548
|
+
},
|
|
549
|
+
],
|
|
550
|
+
evidence,
|
|
551
|
+
validation,
|
|
552
|
+
disposition: findingDisposition,
|
|
553
|
+
provenance,
|
|
554
|
+
extensions: {
|
|
555
|
+
cloudFindingVersion: raw.version ?? null,
|
|
556
|
+
cloudValidationConfidence: commit.validation_confidence ?? null,
|
|
557
|
+
},
|
|
558
|
+
};
|
|
559
|
+
const remediation = text(commit.proposed_patch) ?? text(raw.proposed_patch);
|
|
560
|
+
if (remediation) finding.remediation = remediation;
|
|
561
|
+
return finding;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function repositoryIdentity(value: string): string {
|
|
565
|
+
const trimmed = value
|
|
566
|
+
.trim()
|
|
567
|
+
.replace(/\/+$/, "")
|
|
568
|
+
.replace(/\.git$/, "");
|
|
569
|
+
const scpStyle = trimmed.match(/^[^@]+@([^:]+):(.+)$/);
|
|
570
|
+
if (scpStyle) return `${scpStyle[1]!.toLowerCase()}/${scpStyle[2]!.replace(/^\/+/, "").toLowerCase()}`;
|
|
571
|
+
try {
|
|
572
|
+
const parsed = new URL(trimmed);
|
|
573
|
+
return `${parsed.hostname.toLowerCase()}/${parsed.pathname.replace(/^\/+/, "").toLowerCase()}`;
|
|
574
|
+
} catch {
|
|
575
|
+
return trimmed.toLowerCase();
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
async function assertCloudRepositoryMatchesStore(
|
|
580
|
+
configuration: CodexSecurityCloudConfiguration,
|
|
581
|
+
store: SecurityStore,
|
|
582
|
+
signal?: AbortSignal,
|
|
583
|
+
): Promise<void> {
|
|
584
|
+
const origin = await git.remote.url(store.repositoryRoot, "origin", signal);
|
|
585
|
+
if (!origin) {
|
|
586
|
+
throw new Error(
|
|
587
|
+
"Codex Security cloud import requires a verifiable repository identity; this project has no 'origin' remote",
|
|
588
|
+
);
|
|
589
|
+
}
|
|
590
|
+
if (repositoryIdentity(origin) !== repositoryIdentity(configuration.repositoryUrl)) {
|
|
591
|
+
throw new Error("Codex Security cloud configuration does not match this project's origin remote");
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
function reportForCloudBundle(
|
|
596
|
+
configuration: CodexSecurityCloudConfiguration,
|
|
597
|
+
stats: CodexSecurityCloudStats,
|
|
598
|
+
findings: SecurityFinding[],
|
|
599
|
+
): string {
|
|
600
|
+
const lines = [
|
|
601
|
+
"# Codex Security cloud results",
|
|
602
|
+
"",
|
|
603
|
+
`- Configuration: ${configuration.id}`,
|
|
604
|
+
`- Repository: ${configuration.repositoryUrl}`,
|
|
605
|
+
`- Current step: ${stats.currentStep ?? configuration.currentStep ?? "unknown"}`,
|
|
606
|
+
`- Last scanned commit: ${stats.lastScannedCommit ?? "unknown"}`,
|
|
607
|
+
`- Findings imported: ${findings.length}`,
|
|
608
|
+
"",
|
|
609
|
+
"## Findings",
|
|
610
|
+
"",
|
|
611
|
+
];
|
|
612
|
+
for (const finding of findings) lines.push(`- **${finding.severity.level}** ${finding.title} (${finding.id})`);
|
|
613
|
+
return `${lines.join("\n")}\n`;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
export async function pullCodexSecurityCloudResults(
|
|
617
|
+
input: PullCodexSecurityCloudResultsInput,
|
|
618
|
+
): Promise<SecurityScanBundle> {
|
|
619
|
+
const importedAt = new Date().toISOString();
|
|
620
|
+
const configuration = await input.client.getConfiguration(input.configurationId, input.signal);
|
|
621
|
+
const stats = await input.client.getStats(configuration.id, input.signal);
|
|
622
|
+
await assertCloudRepositoryMatchesStore(configuration, input.store, input.signal);
|
|
623
|
+
const details = await input.client.listFindingDetails(configuration.repositoryUrl, configuration, input.signal);
|
|
624
|
+
const scanId = createSecurityScanId();
|
|
625
|
+
const findings = details.map(item => normalizeFinding(item, scanId, configuration, importedAt));
|
|
626
|
+
const scanSourceIds: Record<string, string> = { cloudConfigurationId: configuration.id };
|
|
627
|
+
if (configuration.sourceId) scanSourceIds.cloudSourceConfigurationId = configuration.sourceId;
|
|
628
|
+
const producer = { ...CLOUD_PRODUCER };
|
|
629
|
+
const revision = stats.lastScannedCommit;
|
|
630
|
+
const bundle: SecurityScanBundle = {
|
|
631
|
+
scan: {
|
|
632
|
+
documentType: "omp-security.scan",
|
|
633
|
+
schemaVersion: "1.0",
|
|
634
|
+
id: scanId,
|
|
635
|
+
projectKey: input.store.projectKey,
|
|
636
|
+
status: "completed",
|
|
637
|
+
createdAt: importedAt,
|
|
638
|
+
completedAt: importedAt,
|
|
639
|
+
target: {
|
|
640
|
+
kind: "imported",
|
|
641
|
+
repositoryRoot: input.store.repositoryRoot,
|
|
642
|
+
displayName: configuration.repositoryUrl,
|
|
643
|
+
includePaths: [],
|
|
644
|
+
excludePaths: [],
|
|
645
|
+
treeDigest: Bun.SHA256.hash(`${configuration.repositoryUrl}\0${revision ?? "unknown"}`, "hex"),
|
|
646
|
+
},
|
|
647
|
+
producer,
|
|
648
|
+
provenance: {
|
|
649
|
+
producer,
|
|
650
|
+
createdAt: configuration.createdAt ?? importedAt,
|
|
651
|
+
importedAt,
|
|
652
|
+
sourceIds: scanSourceIds,
|
|
653
|
+
upstream: { repository: configuration.repositoryUrl },
|
|
654
|
+
metadata: {
|
|
655
|
+
cloudCurrentStep: stats.currentStep ?? configuration.currentStep ?? null,
|
|
656
|
+
cloudUpdatedAt: stats.updatedAt ?? configuration.updatedAt ?? null,
|
|
657
|
+
},
|
|
658
|
+
},
|
|
659
|
+
findingIds: findings.map(item => item.id),
|
|
660
|
+
coverage: {
|
|
661
|
+
mode: "imported",
|
|
662
|
+
completeness: "unknown",
|
|
663
|
+
inventoryStrategy: "imported",
|
|
664
|
+
includePaths: [],
|
|
665
|
+
excludePaths: [],
|
|
666
|
+
surfaces: [],
|
|
667
|
+
explicitExclusions: [],
|
|
668
|
+
deferred: [
|
|
669
|
+
{ id: "cloud-coverage", reason: "Cloud coverage receipts are not exposed by the findings API." },
|
|
670
|
+
],
|
|
671
|
+
},
|
|
672
|
+
reportRef: "report.md",
|
|
673
|
+
sarifRef: "results.sarif",
|
|
674
|
+
},
|
|
675
|
+
findings,
|
|
676
|
+
report: reportForCloudBundle(configuration, stats, findings),
|
|
677
|
+
};
|
|
678
|
+
if (configuration.createdAt) bundle.scan.startedAt = configuration.createdAt;
|
|
679
|
+
if (revision) {
|
|
680
|
+
bundle.scan.target.revision = revision;
|
|
681
|
+
if (bundle.scan.provenance.upstream) bundle.scan.provenance.upstream.revision = revision;
|
|
682
|
+
}
|
|
683
|
+
bundle.sarif = exportSecurityBundleToSarif(bundle);
|
|
684
|
+
await input.store.putBundle(bundle);
|
|
685
|
+
return bundle;
|
|
686
|
+
}
|