opencode-codex-security 0.0.0-stage → 0.1.0
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/CONTRIBUTING.md +15 -0
- package/DISCLOSURE +19 -0
- package/LICENSE +202 -0
- package/README.md +103 -2
- package/SECURITY.md +22 -0
- package/dist/gui-experimental.cjs +17 -0
- package/docs/GLOBAL-CONFIG.md +64 -0
- package/docs/MULTI-SCAN.md +62 -0
- package/docs/PUBLISHING.md +71 -0
- package/docs/RELEASING.md +53 -0
- package/docs/SDK.md +36 -0
- package/fixtures/vulnerable/docs/ARCHITECTURE.md +2 -0
- package/fixtures/vulnerable/src/accounts.js +4 -0
- package/fixtures/vulnerable/src/diagnostics.js +5 -0
- package/fixtures/vulnerable/src/download.js +5 -0
- package/fixtures/vulnerable/src/profile.js +4 -0
- package/package.json +79 -4
- package/src/core/codex-adapter.ts +430 -0
- package/src/core/credential-guard.ts +83 -0
- package/src/core/planned-scan.ts +316 -0
- package/src/core/report-safety.ts +41 -0
- package/src/core/result-summary.ts +512 -0
- package/src/core/run-history.ts +107 -0
- package/src/core/run-telemetry.ts +359 -0
- package/src/core/scan-plan.ts +182 -0
- package/src/core/scan-service.mjs +129 -0
- package/src/core/scanner.mjs +129 -0
- package/src/core/sdk-settings.ts +123 -0
- package/src/core/security-operations.ts +598 -0
- package/src/gui.tsx +102 -0
- package/src/operation-tools.ts +247 -0
- package/src/plan-view.ts +47 -0
- package/src/plugin.ts +346 -0
- package/src/rpc.ts +124 -0
- package/src/scan-display.ts +284 -0
- package/src/security-operations-panel.tsx +551 -0
- package/src/security-panel.tsx +1003 -0
- package/src/security-styles.ts +10 -0
- package/src/security-tui-operations.tsx +343 -0
- package/src/state-order.ts +21 -0
- package/src/tui.tsx +630 -0
- package/src/types.ts +84 -0
|
@@ -0,0 +1,1003 @@
|
|
|
1
|
+
import { createMemo, createSignal, createEffect, on, For, Show, onCleanup } from 'solid-js';
|
|
2
|
+
import { createStore, reconcile } from 'solid-js/store';
|
|
3
|
+
import { asState, type ScanState } from './types';
|
|
4
|
+
import type { ScanInputs, ScanPlan } from './core/scan-plan';
|
|
5
|
+
import { describePlan, latestAvailableDraft } from './plan-view';
|
|
6
|
+
import {
|
|
7
|
+
elapsedLabel,
|
|
8
|
+
findingText,
|
|
9
|
+
runPhaseLabel,
|
|
10
|
+
phaseCounts,
|
|
11
|
+
phaseContext,
|
|
12
|
+
hasPhaseProgress,
|
|
13
|
+
hasDeterminateProgress,
|
|
14
|
+
deepProgressLabel,
|
|
15
|
+
findingsPendingLabel,
|
|
16
|
+
usageLabel,
|
|
17
|
+
usageContext,
|
|
18
|
+
usageRangeLabel,
|
|
19
|
+
activityRows,
|
|
20
|
+
workerActivityRows,
|
|
21
|
+
activityActor,
|
|
22
|
+
activityStatusLabel,
|
|
23
|
+
workerSummary,
|
|
24
|
+
dispatchLabel,
|
|
25
|
+
reconnectLabel,
|
|
26
|
+
resultCountsLabel,
|
|
27
|
+
coverageLabel,
|
|
28
|
+
coverageDetails,
|
|
29
|
+
repositoryMatchingLabel,
|
|
30
|
+
progressLabel,
|
|
31
|
+
projectName,
|
|
32
|
+
runTitle,
|
|
33
|
+
scanScope,
|
|
34
|
+
splitPaths,
|
|
35
|
+
userError,
|
|
36
|
+
} from './scan-display';
|
|
37
|
+
import {
|
|
38
|
+
SecurityOperationsPanel,
|
|
39
|
+
type OperationPort,
|
|
40
|
+
type ValidationSeed,
|
|
41
|
+
} from './security-operations-panel';
|
|
42
|
+
import { securityStyles } from './security-styles';
|
|
43
|
+
|
|
44
|
+
export interface SecurityPort {
|
|
45
|
+
list(): Promise<unknown>;
|
|
46
|
+
prepare(input: ScanInputs): Promise<unknown>;
|
|
47
|
+
start(input: { planId?: string; approveOpenAIInference?: boolean }): Promise<unknown>;
|
|
48
|
+
cancel(input: { runId: string }): Promise<unknown>;
|
|
49
|
+
}
|
|
50
|
+
interface Catalog {
|
|
51
|
+
engine?: string;
|
|
52
|
+
instanceId?: string;
|
|
53
|
+
revision?: number;
|
|
54
|
+
runs: ScanState[];
|
|
55
|
+
drafts: ScanState[];
|
|
56
|
+
preview?: ScanState;
|
|
57
|
+
maxConcurrent: number;
|
|
58
|
+
historyError?: string;
|
|
59
|
+
}
|
|
60
|
+
function readCatalog(value: unknown): Catalog {
|
|
61
|
+
if (!value || typeof value !== 'object' || !('runs' in value) || !Array.isArray(value.runs))
|
|
62
|
+
throw new Error(
|
|
63
|
+
'Could not read scan history. Reopen Security after updating the server plugin.',
|
|
64
|
+
);
|
|
65
|
+
const catalog = value as Catalog;
|
|
66
|
+
return {
|
|
67
|
+
...catalog,
|
|
68
|
+
runs: catalog.runs.map(asState),
|
|
69
|
+
drafts: (catalog.drafts ?? []).map(asState),
|
|
70
|
+
maxConcurrent: catalog.maxConcurrent ?? 2,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const clockTime = (value: string) =>
|
|
74
|
+
new Date(value).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit', second: '2-digit' });
|
|
75
|
+
const startedTime = (value: string | null) =>
|
|
76
|
+
value ? new Date(value).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '';
|
|
77
|
+
|
|
78
|
+
/** Actual Desktop panel, also mounted by the isolated mock visual harness. */
|
|
79
|
+
export function SecurityPanel(props: {
|
|
80
|
+
directory: string;
|
|
81
|
+
port: SecurityPort;
|
|
82
|
+
operations?: OperationPort;
|
|
83
|
+
pollMs?: number;
|
|
84
|
+
}) {
|
|
85
|
+
const [catalogStore, updateCatalog] = createStore<Catalog>({
|
|
86
|
+
runs: [],
|
|
87
|
+
drafts: [],
|
|
88
|
+
maxConcurrent: 2,
|
|
89
|
+
});
|
|
90
|
+
const catalog = () => catalogStore;
|
|
91
|
+
const setCatalog = (next: Catalog | ((previous: Catalog) => Catalog)) =>
|
|
92
|
+
updateCatalog(
|
|
93
|
+
reconcile(typeof next === 'function' ? next(catalogStore) : next, {
|
|
94
|
+
key: 'runId',
|
|
95
|
+
merge: true,
|
|
96
|
+
}),
|
|
97
|
+
);
|
|
98
|
+
const [loaded, setLoaded] = createSignal(false);
|
|
99
|
+
const [view, setView] = createSignal<'scans' | 'policy' | 'validation'>('scans');
|
|
100
|
+
const [pendingQuestions, setPendingQuestions] = createSignal(0);
|
|
101
|
+
const [validationSeed, setValidationSeed] = createSignal<ValidationSeed>();
|
|
102
|
+
const [selectedId, selectRun] = createSignal<string>();
|
|
103
|
+
const [composer, setComposer] = createSignal(true);
|
|
104
|
+
const [preview, setPreview] = createSignal<ScanPlan>();
|
|
105
|
+
const [repository, setRepository] = createSignal(props.directory);
|
|
106
|
+
const [mode, setMode] = createSignal<'default' | 'standard' | 'deep'>('default');
|
|
107
|
+
const [focus, setFocus] = createSignal('');
|
|
108
|
+
const [paths, setPaths] = createSignal('');
|
|
109
|
+
const [documents, setDocuments] = createSignal('');
|
|
110
|
+
const [busy, setBusy] = createSignal<'prepare' | 'start' | 'cancel' | null>(null);
|
|
111
|
+
const [error, setError] = createSignal('');
|
|
112
|
+
const [connectionError, setConnectionError] = createSignal('');
|
|
113
|
+
const [now, setNow] = createSignal(Date.now());
|
|
114
|
+
let detailElement: HTMLElement | undefined;
|
|
115
|
+
let historyElement: HTMLDetailsElement | undefined;
|
|
116
|
+
let stopped = false,
|
|
117
|
+
refreshing = false,
|
|
118
|
+
touched = false,
|
|
119
|
+
refreshSequence = 0;
|
|
120
|
+
let acceptedSequence = 0,
|
|
121
|
+
draftRevision = 0;
|
|
122
|
+
const activeCount = createMemo(() => catalog().runs.filter((r) => r.status === 'running').length);
|
|
123
|
+
const current = createMemo(
|
|
124
|
+
() => catalog().runs.find((r) => r.runId === selectedId()) ?? catalog().runs[0],
|
|
125
|
+
);
|
|
126
|
+
createEffect(
|
|
127
|
+
on(
|
|
128
|
+
() => current()?.runId,
|
|
129
|
+
() => {
|
|
130
|
+
detailElement
|
|
131
|
+
?.querySelectorAll('details[open]')
|
|
132
|
+
.forEach((detail) => detail.removeAttribute('open'));
|
|
133
|
+
if (historyElement) historyElement.open = false;
|
|
134
|
+
},
|
|
135
|
+
),
|
|
136
|
+
);
|
|
137
|
+
const availableDraft = createMemo(() => latestAvailableDraft(catalog(), now()));
|
|
138
|
+
const fixture = createMemo(
|
|
139
|
+
() =>
|
|
140
|
+
catalog().engine?.startsWith('local-patterns') ||
|
|
141
|
+
catalog().runs[0]?.engine?.startsWith('local-patterns') ||
|
|
142
|
+
catalog().drafts[0]?.engine?.startsWith('local-patterns'),
|
|
143
|
+
);
|
|
144
|
+
const refresh = async () => {
|
|
145
|
+
if (refreshing || stopped) return;
|
|
146
|
+
refreshing = true;
|
|
147
|
+
const sequence = ++refreshSequence;
|
|
148
|
+
try {
|
|
149
|
+
const next = readCatalog(await props.port.list());
|
|
150
|
+
if (stopped || sequence < acceptedSequence) return;
|
|
151
|
+
acceptedSequence = sequence;
|
|
152
|
+
setCatalog(next);
|
|
153
|
+
setConnectionError('');
|
|
154
|
+
if (props.operations) {
|
|
155
|
+
try {
|
|
156
|
+
const result = (await props.operations.list()) as { operations?: { status: string }[] };
|
|
157
|
+
if (!stopped)
|
|
158
|
+
setPendingQuestions(
|
|
159
|
+
(result.operations ?? []).filter((op) => op.status === 'awaiting_answers').length,
|
|
160
|
+
);
|
|
161
|
+
} catch {
|
|
162
|
+
/* A secondary operation refresh must not hide scan state. */
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (!loaded()) {
|
|
166
|
+
setLoaded(true);
|
|
167
|
+
if (!touched && next.runs.length) setComposer(false);
|
|
168
|
+
}
|
|
169
|
+
} catch (e) {
|
|
170
|
+
if (!stopped) setConnectionError(userError(e));
|
|
171
|
+
} finally {
|
|
172
|
+
refreshing = false;
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
const edit = () => {
|
|
176
|
+
draftRevision++;
|
|
177
|
+
touched = true;
|
|
178
|
+
setPreview(undefined);
|
|
179
|
+
};
|
|
180
|
+
const openComposer = () => {
|
|
181
|
+
edit();
|
|
182
|
+
setRepository(props.directory);
|
|
183
|
+
setMode('default');
|
|
184
|
+
setFocus('');
|
|
185
|
+
setPaths('');
|
|
186
|
+
setDocuments('');
|
|
187
|
+
setComposer(true);
|
|
188
|
+
setError('');
|
|
189
|
+
};
|
|
190
|
+
const adoptDraft = (plan: ScanPlan) => {
|
|
191
|
+
edit();
|
|
192
|
+
setRepository(plan.repository);
|
|
193
|
+
setMode(plan.inputs.mode ?? 'default');
|
|
194
|
+
setFocus(plan.scanPrompt);
|
|
195
|
+
setPaths(plan.scopePaths.join('\n'));
|
|
196
|
+
setDocuments(plan.contextFiles.map((f) => f.path).join('\n'));
|
|
197
|
+
setPreview(plan);
|
|
198
|
+
setComposer(true);
|
|
199
|
+
setError('');
|
|
200
|
+
};
|
|
201
|
+
const prepare = async (event: SubmitEvent) => {
|
|
202
|
+
event.preventDefault();
|
|
203
|
+
if (busy() || stopped) return;
|
|
204
|
+
setBusy('prepare');
|
|
205
|
+
setError('');
|
|
206
|
+
const preparingRevision = draftRevision;
|
|
207
|
+
try {
|
|
208
|
+
const input: ScanInputs = {
|
|
209
|
+
repository: repository().trim() || props.directory,
|
|
210
|
+
scanPrompt: focus(),
|
|
211
|
+
scopePaths: splitPaths(paths()),
|
|
212
|
+
contextFiles: splitPaths(documents()),
|
|
213
|
+
...(mode() !== 'default' ? { mode: mode() as 'standard' | 'deep' } : {}),
|
|
214
|
+
};
|
|
215
|
+
const next = asState(await props.port.prepare(input));
|
|
216
|
+
if (stopped) return;
|
|
217
|
+
if (!next.plan)
|
|
218
|
+
throw new Error('The server did not return a scan preview. Nothing was started.');
|
|
219
|
+
if (draftRevision === preparingRevision) setPreview(next.plan);
|
|
220
|
+
await refresh();
|
|
221
|
+
} catch (e) {
|
|
222
|
+
if (!stopped) setError(userError(e));
|
|
223
|
+
} finally {
|
|
224
|
+
if (!stopped) setBusy(null);
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
const start = async () => {
|
|
228
|
+
if (busy() || stopped || (!preview() && !fixture())) return;
|
|
229
|
+
setBusy('start');
|
|
230
|
+
setError('');
|
|
231
|
+
try {
|
|
232
|
+
const plan = preview();
|
|
233
|
+
const run = asState(
|
|
234
|
+
await props.port.start(plan ? { planId: plan.id, approveOpenAIInference: true } : {}),
|
|
235
|
+
);
|
|
236
|
+
if (stopped) return;
|
|
237
|
+
acceptedSequence = ++refreshSequence;
|
|
238
|
+
selectRun(run.runId);
|
|
239
|
+
setComposer(false);
|
|
240
|
+
setPreview(undefined);
|
|
241
|
+
setCatalog((old) => ({
|
|
242
|
+
...old,
|
|
243
|
+
runs: [run, ...old.runs.filter((r) => r.runId !== run.runId)],
|
|
244
|
+
}));
|
|
245
|
+
await refresh();
|
|
246
|
+
} catch (e) {
|
|
247
|
+
if (!stopped) setError(userError(e));
|
|
248
|
+
} finally {
|
|
249
|
+
if (!stopped) setBusy(null);
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
const cancel = async (run: ScanState) => {
|
|
253
|
+
if (busy() || stopped || !run.runId) return;
|
|
254
|
+
setBusy('cancel');
|
|
255
|
+
setError('');
|
|
256
|
+
try {
|
|
257
|
+
await props.port.cancel({ runId: run.runId });
|
|
258
|
+
if (!stopped) await refresh();
|
|
259
|
+
} catch (e) {
|
|
260
|
+
if (!stopped) setError(userError(e));
|
|
261
|
+
} finally {
|
|
262
|
+
if (!stopped) setBusy(null);
|
|
263
|
+
}
|
|
264
|
+
};
|
|
265
|
+
void refresh();
|
|
266
|
+
const timer = setInterval(() => {
|
|
267
|
+
setNow(Date.now());
|
|
268
|
+
void refresh();
|
|
269
|
+
}, props.pollMs ?? 1500);
|
|
270
|
+
onCleanup(() => {
|
|
271
|
+
stopped = true;
|
|
272
|
+
clearInterval(timer);
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
return (
|
|
276
|
+
<section class="gill-security" aria-label="Security scans">
|
|
277
|
+
<style>{securityStyles}</style>
|
|
278
|
+
<div class="gs-body">
|
|
279
|
+
<header class="gs-header">
|
|
280
|
+
<div>
|
|
281
|
+
<h2>Security</h2>
|
|
282
|
+
<p class="gs-muted">
|
|
283
|
+
Workspace · {projectName(props.directory)}
|
|
284
|
+
{activeCount() > 0 ? ` · ${activeCount()} running` : ''}
|
|
285
|
+
</p>
|
|
286
|
+
</div>
|
|
287
|
+
<Show when={view() === 'scans' && !composer()}>
|
|
288
|
+
<button class="gs-button gs-primary" onClick={openComposer}>
|
|
289
|
+
New scan
|
|
290
|
+
</button>
|
|
291
|
+
</Show>
|
|
292
|
+
</header>
|
|
293
|
+
<details class="gs-workspace">
|
|
294
|
+
<summary>Workspace and history</summary>
|
|
295
|
+
<p class="gs-id">{props.directory}</p>
|
|
296
|
+
<p>History belongs to this workspace, including runs with a different target.</p>
|
|
297
|
+
<p>
|
|
298
|
+
Global settings share model and scan defaults, not a scan target or combined history.
|
|
299
|
+
</p>
|
|
300
|
+
</details>
|
|
301
|
+
<Show when={props.operations && loaded() && !fixture()}>
|
|
302
|
+
<nav class="gs-tabs" aria-label="Security actions">
|
|
303
|
+
<button
|
|
304
|
+
aria-current={view() === 'scans' ? 'page' : undefined}
|
|
305
|
+
onClick={() => setView('scans')}
|
|
306
|
+
>
|
|
307
|
+
Scans
|
|
308
|
+
</button>
|
|
309
|
+
<Show when={props.operations?.capabilities?.policy ?? !!props.operations}>
|
|
310
|
+
<button
|
|
311
|
+
aria-current={view() === 'policy' ? 'page' : undefined}
|
|
312
|
+
onClick={() => setView('policy')}
|
|
313
|
+
>
|
|
314
|
+
Policy{pendingQuestions() ? ` · ${pendingQuestions()} waiting` : ''}
|
|
315
|
+
</button>
|
|
316
|
+
</Show>
|
|
317
|
+
<Show when={props.operations?.capabilities?.validation ?? !!props.operations}>
|
|
318
|
+
<button
|
|
319
|
+
aria-current={view() === 'validation' ? 'page' : undefined}
|
|
320
|
+
onClick={() => {
|
|
321
|
+
setValidationSeed(undefined);
|
|
322
|
+
setView('validation');
|
|
323
|
+
}}
|
|
324
|
+
>
|
|
325
|
+
Validate
|
|
326
|
+
</button>
|
|
327
|
+
</Show>
|
|
328
|
+
</nav>
|
|
329
|
+
</Show>
|
|
330
|
+
<Show when={view() !== 'scans' ? view() : undefined} keyed>
|
|
331
|
+
{(kind) => (
|
|
332
|
+
<SecurityOperationsPanel
|
|
333
|
+
kind={kind as 'policy' | 'validation'}
|
|
334
|
+
directory={props.directory}
|
|
335
|
+
port={props.operations!}
|
|
336
|
+
seed={validationSeed()}
|
|
337
|
+
/>
|
|
338
|
+
)}
|
|
339
|
+
</Show>
|
|
340
|
+
<Show when={view() === 'scans'}>
|
|
341
|
+
<Show when={pendingQuestions()}>
|
|
342
|
+
<div class="gs-pending">
|
|
343
|
+
<span>A policy draft needs your input.</span>
|
|
344
|
+
<button class="gs-button" onClick={() => setView('policy')}>
|
|
345
|
+
Answer questions
|
|
346
|
+
</button>
|
|
347
|
+
</div>
|
|
348
|
+
</Show>
|
|
349
|
+
<Show when={connectionError()}>
|
|
350
|
+
<div class="gs-notice gs-error" role="alert">
|
|
351
|
+
{connectionError()}{' '}
|
|
352
|
+
<button class="gs-link" onClick={() => void refresh()}>
|
|
353
|
+
Retry connection
|
|
354
|
+
</button>
|
|
355
|
+
</div>
|
|
356
|
+
</Show>
|
|
357
|
+
<Show when={catalog().historyError}>
|
|
358
|
+
<p class="gs-notice gs-error" role="alert">
|
|
359
|
+
{catalog().historyError}
|
|
360
|
+
</p>
|
|
361
|
+
</Show>
|
|
362
|
+
<Show when={error()}>
|
|
363
|
+
<p class="gs-notice gs-error" role="alert">
|
|
364
|
+
{error()}
|
|
365
|
+
</p>
|
|
366
|
+
</Show>
|
|
367
|
+
<Show when={!composer() && availableDraft()?.plan}>
|
|
368
|
+
{(_plan) => (
|
|
369
|
+
<div class="gs-pending">
|
|
370
|
+
<span>Prepared target: {availableDraft()?.plan?.repository}</span>
|
|
371
|
+
<button class="gs-button" onClick={() => adoptDraft(availableDraft()!.plan!)}>
|
|
372
|
+
Review draft
|
|
373
|
+
</button>
|
|
374
|
+
</div>
|
|
375
|
+
)}
|
|
376
|
+
</Show>
|
|
377
|
+
<Show when={composer()}>
|
|
378
|
+
<section class="gs-form" aria-label="New scan">
|
|
379
|
+
<div class="gs-section-heading">
|
|
380
|
+
<h3>New scan</h3>
|
|
381
|
+
<Show when={catalog().runs.length}>
|
|
382
|
+
<button class="gs-link" disabled={!!busy()} onClick={() => setComposer(false)}>
|
|
383
|
+
Close
|
|
384
|
+
</button>
|
|
385
|
+
</Show>
|
|
386
|
+
</div>
|
|
387
|
+
<Show
|
|
388
|
+
when={!fixture()}
|
|
389
|
+
fallback={
|
|
390
|
+
<>
|
|
391
|
+
<p class="gs-muted gs-small">
|
|
392
|
+
Synthetic fixture test. Local pattern matching only, with no source upload or
|
|
393
|
+
model calls.
|
|
394
|
+
</p>
|
|
395
|
+
<button
|
|
396
|
+
class="gs-button gs-primary"
|
|
397
|
+
disabled={!!busy()}
|
|
398
|
+
onClick={() => void start()}
|
|
399
|
+
>
|
|
400
|
+
Scan fixture
|
|
401
|
+
</button>
|
|
402
|
+
</>
|
|
403
|
+
}
|
|
404
|
+
>
|
|
405
|
+
<Show
|
|
406
|
+
when={preview()}
|
|
407
|
+
fallback={
|
|
408
|
+
<form onSubmit={(event) => void prepare(event)}>
|
|
409
|
+
<Show when={availableDraft()?.plan}>
|
|
410
|
+
<button
|
|
411
|
+
type="button"
|
|
412
|
+
class="gs-link"
|
|
413
|
+
onClick={() => adoptDraft(availableDraft()!.plan!)}
|
|
414
|
+
>
|
|
415
|
+
Review prepared scan · {availableDraft()?.plan?.repository}
|
|
416
|
+
</button>
|
|
417
|
+
</Show>
|
|
418
|
+
<label>
|
|
419
|
+
Target repository
|
|
420
|
+
<input
|
|
421
|
+
value={repository()}
|
|
422
|
+
onInput={(e) => {
|
|
423
|
+
edit();
|
|
424
|
+
setRepository(e.currentTarget.value);
|
|
425
|
+
setPaths('');
|
|
426
|
+
setDocuments('');
|
|
427
|
+
}}
|
|
428
|
+
required
|
|
429
|
+
aria-label="Repository"
|
|
430
|
+
spellcheck={false}
|
|
431
|
+
/>
|
|
432
|
+
<span class="gs-help">
|
|
433
|
+
Defaults to this workspace. Changing the target clears scope and context
|
|
434
|
+
files.
|
|
435
|
+
</span>
|
|
436
|
+
</label>
|
|
437
|
+
<label>
|
|
438
|
+
Scan depth
|
|
439
|
+
<select
|
|
440
|
+
value={mode()}
|
|
441
|
+
onChange={(e) => {
|
|
442
|
+
edit();
|
|
443
|
+
setMode(e.currentTarget.value as 'default' | 'standard' | 'deep');
|
|
444
|
+
}}
|
|
445
|
+
aria-label="Scan depth"
|
|
446
|
+
>
|
|
447
|
+
<option value="default">Use global default</option>
|
|
448
|
+
<option value="standard">Standard</option>
|
|
449
|
+
<option value="deep">Deep</option>
|
|
450
|
+
</select>
|
|
451
|
+
<span class="gs-help">
|
|
452
|
+
Deep can take longer and cost more. This choice applies only to this scan.
|
|
453
|
+
</span>
|
|
454
|
+
</label>
|
|
455
|
+
<label>
|
|
456
|
+
What should this scan focus on? <span class="gs-muted">Optional</span>
|
|
457
|
+
<textarea
|
|
458
|
+
value={focus()}
|
|
459
|
+
maxLength={8000}
|
|
460
|
+
onInput={(e) => {
|
|
461
|
+
edit();
|
|
462
|
+
setFocus(e.currentTarget.value);
|
|
463
|
+
}}
|
|
464
|
+
placeholder="For example, review authorization and tenant isolation."
|
|
465
|
+
aria-label="Scan focus"
|
|
466
|
+
/>
|
|
467
|
+
</label>
|
|
468
|
+
<details>
|
|
469
|
+
<summary>Scope and supporting documents</summary>
|
|
470
|
+
<label>
|
|
471
|
+
Paths to review
|
|
472
|
+
<textarea
|
|
473
|
+
value={paths()}
|
|
474
|
+
onInput={(e) => {
|
|
475
|
+
edit();
|
|
476
|
+
setPaths(e.currentTarget.value);
|
|
477
|
+
}}
|
|
478
|
+
placeholder={'src/auth\nsrc/api'}
|
|
479
|
+
aria-label="Scope paths"
|
|
480
|
+
/>
|
|
481
|
+
<span class="gs-help">
|
|
482
|
+
One repository-relative path per line. Leave blank for the whole
|
|
483
|
+
repository.
|
|
484
|
+
</span>
|
|
485
|
+
</label>
|
|
486
|
+
<label>
|
|
487
|
+
Context files
|
|
488
|
+
<textarea
|
|
489
|
+
value={documents()}
|
|
490
|
+
onInput={(e) => {
|
|
491
|
+
edit();
|
|
492
|
+
setDocuments(e.currentTarget.value);
|
|
493
|
+
}}
|
|
494
|
+
placeholder={'docs/architecture.md\nSECURITY.md'}
|
|
495
|
+
aria-label="Context files"
|
|
496
|
+
/>
|
|
497
|
+
<span class="gs-help">
|
|
498
|
+
Optional text documents within this repository. Do not include secrets.
|
|
499
|
+
</span>
|
|
500
|
+
</label>
|
|
501
|
+
</details>
|
|
502
|
+
<div class="gs-actions">
|
|
503
|
+
<button
|
|
504
|
+
type="submit"
|
|
505
|
+
class="gs-button gs-primary"
|
|
506
|
+
disabled={!!busy() || !loaded()}
|
|
507
|
+
>
|
|
508
|
+
{busy() === 'prepare' ? 'Preparing preview…' : 'Review scan'}
|
|
509
|
+
</button>
|
|
510
|
+
<span class="gs-muted gs-small">Nothing is sent to OpenAI yet.</span>
|
|
511
|
+
</div>
|
|
512
|
+
</form>
|
|
513
|
+
}
|
|
514
|
+
>
|
|
515
|
+
{(plan) => (
|
|
516
|
+
<div class="gs-review">
|
|
517
|
+
<h3>Review this scan</h3>
|
|
518
|
+
<dl>
|
|
519
|
+
<dt>Repository</dt>
|
|
520
|
+
<dd>{plan().repository}</dd>
|
|
521
|
+
<dt>Scope</dt>
|
|
522
|
+
<dd>{plan().scopePaths.join(', ') || 'Whole repository'}</dd>
|
|
523
|
+
<dt>Depth</dt>
|
|
524
|
+
<dd>{plan().settings.scan.mode === 'deep' ? 'Deep' : 'Standard'}</dd>
|
|
525
|
+
<Show when={plan().scanPrompt}>
|
|
526
|
+
<dt>Focus</dt>
|
|
527
|
+
<dd>{plan().scanPrompt}</dd>
|
|
528
|
+
</Show>
|
|
529
|
+
<Show when={plan().contextFiles.length}>
|
|
530
|
+
<dt>Supporting documents</dt>
|
|
531
|
+
<dd>
|
|
532
|
+
{plan()
|
|
533
|
+
.contextFiles.map((f) => f.path)
|
|
534
|
+
.join(', ')}
|
|
535
|
+
</dd>
|
|
536
|
+
</Show>
|
|
537
|
+
</dl>
|
|
538
|
+
<details class="gs-advanced">
|
|
539
|
+
<summary>Model and advanced settings</summary>
|
|
540
|
+
<dl class="gs-details-list">
|
|
541
|
+
<dt>Model</dt>
|
|
542
|
+
<dd>
|
|
543
|
+
{plan().settings.codex.model} · {plan().settings.codex.reasoningEffort}{' '}
|
|
544
|
+
reasoning
|
|
545
|
+
</dd>
|
|
546
|
+
<dt>Daybreak</dt>
|
|
547
|
+
<dd>{plan().settings.codex.daybreak ? 'On' : 'Off'}</dd>
|
|
548
|
+
<dt>Global configuration</dt>
|
|
549
|
+
<dd>{plan().globalConfigFile}</dd>
|
|
550
|
+
<dt>Settings scope</dt>
|
|
551
|
+
<dd>
|
|
552
|
+
Shared defaults only. This target and history belong to this workspace.
|
|
553
|
+
</dd>
|
|
554
|
+
</dl>
|
|
555
|
+
<p class="gs-muted">Model and account access have not been verified.</p>
|
|
556
|
+
<details>
|
|
557
|
+
<summary>Full approval details</summary>
|
|
558
|
+
<pre>{describePlan(plan())}</pre>
|
|
559
|
+
</details>
|
|
560
|
+
</details>
|
|
561
|
+
<div class="gs-approval">
|
|
562
|
+
<Show
|
|
563
|
+
when={plan().mock}
|
|
564
|
+
fallback={
|
|
565
|
+
<>
|
|
566
|
+
<p>
|
|
567
|
+
Repository source and listed context will be sent to OpenAI. Results
|
|
568
|
+
are stored locally.
|
|
569
|
+
</p>
|
|
570
|
+
<p>
|
|
571
|
+
Estimated cost stop:{' '}
|
|
572
|
+
<strong>USD {plan().settings.scan.maxCostUsd}</strong> for this
|
|
573
|
+
scan. This is not a hard cap; in-flight usage may exceed it.
|
|
574
|
+
</p>
|
|
575
|
+
<p>
|
|
576
|
+
Only the focus and documents above are added as context, not your
|
|
577
|
+
conversation. Scope guides the review; it is not a filesystem
|
|
578
|
+
sandbox.
|
|
579
|
+
</p>
|
|
580
|
+
</>
|
|
581
|
+
}
|
|
582
|
+
>
|
|
583
|
+
<p>
|
|
584
|
+
Mock SDK test. Synthetic responses only; no model calls or source
|
|
585
|
+
uploads.
|
|
586
|
+
</p>
|
|
587
|
+
</Show>
|
|
588
|
+
</div>
|
|
589
|
+
<Show when={activeCount() >= catalog().maxConcurrent}>
|
|
590
|
+
<p role="status" class="gs-small gs-muted">
|
|
591
|
+
{catalog().maxConcurrent} scans are already running. Start this scan after
|
|
592
|
+
one finishes.
|
|
593
|
+
</p>
|
|
594
|
+
</Show>
|
|
595
|
+
<Show when={Date.parse(plan().expiresAt) <= now()}>
|
|
596
|
+
<p class="gs-small gs-error">
|
|
597
|
+
This preview has expired. Edit and review it again.
|
|
598
|
+
</p>
|
|
599
|
+
</Show>
|
|
600
|
+
<div class="gs-actions">
|
|
601
|
+
<button
|
|
602
|
+
class="gs-button gs-primary"
|
|
603
|
+
disabled={
|
|
604
|
+
!!busy() ||
|
|
605
|
+
activeCount() >= catalog().maxConcurrent ||
|
|
606
|
+
Date.parse(plan().expiresAt) <= now()
|
|
607
|
+
}
|
|
608
|
+
onClick={() => void start()}
|
|
609
|
+
>
|
|
610
|
+
{busy() === 'start'
|
|
611
|
+
? 'Starting…'
|
|
612
|
+
: plan().mock
|
|
613
|
+
? 'Start mock scan'
|
|
614
|
+
: 'Approve and start'}
|
|
615
|
+
</button>
|
|
616
|
+
<button
|
|
617
|
+
class="gs-button"
|
|
618
|
+
disabled={!!busy()}
|
|
619
|
+
onClick={() => setPreview(undefined)}
|
|
620
|
+
>
|
|
621
|
+
Edit scan
|
|
622
|
+
</button>
|
|
623
|
+
</div>
|
|
624
|
+
</div>
|
|
625
|
+
)}
|
|
626
|
+
</Show>
|
|
627
|
+
</Show>
|
|
628
|
+
</section>
|
|
629
|
+
</Show>
|
|
630
|
+
<Show
|
|
631
|
+
when={catalog().runs.length}
|
|
632
|
+
fallback={
|
|
633
|
+
<Show when={loaded() && !composer()}>
|
|
634
|
+
<div class="gs-empty">
|
|
635
|
+
<h3>No scans yet</h3>
|
|
636
|
+
<p>Start a scan to review findings and follow its activity here.</p>
|
|
637
|
+
</div>
|
|
638
|
+
</Show>
|
|
639
|
+
}
|
|
640
|
+
>
|
|
641
|
+
<details
|
|
642
|
+
ref={(element) => (historyElement = element)}
|
|
643
|
+
class="gs-runs"
|
|
644
|
+
aria-label="Scan history"
|
|
645
|
+
>
|
|
646
|
+
<summary>
|
|
647
|
+
Switch scan <span>{catalog().runs.length} recent</span>
|
|
648
|
+
</summary>
|
|
649
|
+
<p class="gs-muted">Runs recorded in this workspace. Each target is shown below.</p>
|
|
650
|
+
<ul class="gs-run-list">
|
|
651
|
+
<For each={catalog().runs}>
|
|
652
|
+
{(run) => (
|
|
653
|
+
<li>
|
|
654
|
+
<button
|
|
655
|
+
class="gs-run-button"
|
|
656
|
+
aria-pressed={current()?.runId === run.runId}
|
|
657
|
+
onClick={() => selectRun(run.runId)}
|
|
658
|
+
>
|
|
659
|
+
<div class="gs-run-top">
|
|
660
|
+
<span class="gs-run-title">{runTitle(run)}</span>
|
|
661
|
+
<span class="gs-status" data-status={run.status}>
|
|
662
|
+
{run.status === 'error' ? 'Failed' : run.status}
|
|
663
|
+
</span>
|
|
664
|
+
</div>
|
|
665
|
+
<div class="gs-run-target">Target: {run.repository}</div>
|
|
666
|
+
<div class="gs-run-meta">
|
|
667
|
+
{run.mode ?? run.plan?.settings.scan.mode ?? 'Fixture'} · {scanScope(run)}{' '}
|
|
668
|
+
· {startedTime(run.startedAt)}
|
|
669
|
+
{run.status === 'completed'
|
|
670
|
+
? ` · ${run.findings.length} ${run.findings.length === 1 ? 'finding' : 'findings'} shown`
|
|
671
|
+
: ''}
|
|
672
|
+
</div>
|
|
673
|
+
</button>
|
|
674
|
+
</li>
|
|
675
|
+
)}
|
|
676
|
+
</For>
|
|
677
|
+
</ul>
|
|
678
|
+
</details>
|
|
679
|
+
</Show>
|
|
680
|
+
<Show when={current()}>
|
|
681
|
+
{(run) => (
|
|
682
|
+
<article
|
|
683
|
+
ref={(element) => (detailElement = element)}
|
|
684
|
+
class="gs-detail"
|
|
685
|
+
aria-label="Selected scan"
|
|
686
|
+
>
|
|
687
|
+
<div class="gs-detail-title">
|
|
688
|
+
<h3>{runTitle(run())}</h3>
|
|
689
|
+
<p>Target: {run().repository}</p>
|
|
690
|
+
<p>
|
|
691
|
+
{run().mode ?? run().plan?.settings.scan.mode ?? 'Fixture'} · {scanScope(run())}
|
|
692
|
+
</p>
|
|
693
|
+
</div>
|
|
694
|
+
<div class="gs-progress-heading" role="status" aria-live="polite">
|
|
695
|
+
<h3>{runPhaseLabel(run())}</h3>
|
|
696
|
+
<span>{elapsedLabel(run().startedAt, run().finishedAt, now())}</span>
|
|
697
|
+
</div>
|
|
698
|
+
<Show when={hasDeterminateProgress(run())}>
|
|
699
|
+
<progress
|
|
700
|
+
class="gs-progress"
|
|
701
|
+
aria-label={`${runPhaseLabel(run())}: reported phase progress`}
|
|
702
|
+
value={phaseCounts(run()).done}
|
|
703
|
+
max={phaseCounts(run()).total}
|
|
704
|
+
/>
|
|
705
|
+
</Show>
|
|
706
|
+
<Show when={hasPhaseProgress(run())}>
|
|
707
|
+
<div class="gs-progress-meta">
|
|
708
|
+
<span>{progressLabel(run())}</span>
|
|
709
|
+
<span>{phaseContext(run())}</span>
|
|
710
|
+
</div>
|
|
711
|
+
</Show>
|
|
712
|
+
<div class="gs-scan-actions">
|
|
713
|
+
<span class="gs-muted gs-small">{usageLabel(run())}</span>
|
|
714
|
+
<Show when={run().status === 'running'}>
|
|
715
|
+
<button class="gs-link" disabled={!!busy()} onClick={() => void cancel(run())}>
|
|
716
|
+
{busy() === 'cancel' ? 'Requesting stop…' : 'Cancel scan'}
|
|
717
|
+
</button>
|
|
718
|
+
</Show>
|
|
719
|
+
</div>
|
|
720
|
+
<Show when={run().status === 'running'}>
|
|
721
|
+
<p class="gs-live-note">{findingsPendingLabel(run())}</p>
|
|
722
|
+
</Show>
|
|
723
|
+
<Show when={run().error}>
|
|
724
|
+
<p class="gs-notice gs-error" role="alert">
|
|
725
|
+
{run().error}
|
|
726
|
+
</p>
|
|
727
|
+
</Show>
|
|
728
|
+
<Show when={run().status === 'cancelled'}>
|
|
729
|
+
<p class="gs-notice">
|
|
730
|
+
Cancelled. This scan is incomplete and is not a clean result.
|
|
731
|
+
</p>
|
|
732
|
+
</Show>
|
|
733
|
+
<Show when={run().status === 'completed' && run().findings.length === 0}>
|
|
734
|
+
<div class="gs-empty">
|
|
735
|
+
<h3>No reportable findings shown</h3>
|
|
736
|
+
<p>No findings does not guarantee security. Check coverage below.</p>
|
|
737
|
+
</div>
|
|
738
|
+
</Show>
|
|
739
|
+
<Show when={run().findings.length}>
|
|
740
|
+
<section class="gs-findings" aria-label="Scan findings">
|
|
741
|
+
<div class="gs-section-heading">
|
|
742
|
+
<h3>Findings</h3>
|
|
743
|
+
<span>{run().findings.length} to review</span>
|
|
744
|
+
</div>
|
|
745
|
+
<For each={run().findings}>
|
|
746
|
+
{(finding) => (
|
|
747
|
+
<details>
|
|
748
|
+
<summary>
|
|
749
|
+
<span class="gs-severity" data-severity={finding.severity}>
|
|
750
|
+
{finding.severity}
|
|
751
|
+
</span>{' '}
|
|
752
|
+
{finding.title}
|
|
753
|
+
</summary>
|
|
754
|
+
<div class="gs-finding-location">
|
|
755
|
+
{finding.path}:{finding.line} · {finding.cwe}
|
|
756
|
+
</div>
|
|
757
|
+
<p>{finding.summary}</p>
|
|
758
|
+
<Show when={finding.evidence}>
|
|
759
|
+
<pre>{finding.evidence}</pre>
|
|
760
|
+
</Show>
|
|
761
|
+
<strong class="gs-small">Suggested next step</strong>
|
|
762
|
+
<p>{finding.remediation}</p>
|
|
763
|
+
<p class="gs-muted gs-small">
|
|
764
|
+
Confidence: {finding.confidence}. Review the evidence before making
|
|
765
|
+
changes.
|
|
766
|
+
</p>
|
|
767
|
+
<Show
|
|
768
|
+
when={
|
|
769
|
+
props.operations &&
|
|
770
|
+
(props.operations.capabilities?.validation ?? true) &&
|
|
771
|
+
!fixture()
|
|
772
|
+
}
|
|
773
|
+
>
|
|
774
|
+
<button
|
|
775
|
+
class="gs-button"
|
|
776
|
+
onClick={() => {
|
|
777
|
+
setValidationSeed({
|
|
778
|
+
repository: run().repository,
|
|
779
|
+
finding: findingText(finding),
|
|
780
|
+
});
|
|
781
|
+
setView('validation');
|
|
782
|
+
}}
|
|
783
|
+
>
|
|
784
|
+
Validate this finding
|
|
785
|
+
</button>
|
|
786
|
+
</Show>
|
|
787
|
+
</details>
|
|
788
|
+
)}
|
|
789
|
+
</For>
|
|
790
|
+
</section>
|
|
791
|
+
</Show>
|
|
792
|
+
<Show when={run().status === 'completed'}>
|
|
793
|
+
<section class="gs-outcome" aria-label="Scan outcome">
|
|
794
|
+
<p class="gs-outcome-summary">{resultCountsLabel(run())}</p>
|
|
795
|
+
<details class="gs-coverage">
|
|
796
|
+
<summary>Coverage and repository matching</summary>
|
|
797
|
+
<p>{coverageLabel(run())}</p>
|
|
798
|
+
<p class="gs-muted">Reported metadata, not proof of exhaustive review.</p>
|
|
799
|
+
<dl class="gs-details-list">
|
|
800
|
+
<For each={coverageDetails(run())}>
|
|
801
|
+
{(row) => (
|
|
802
|
+
<>
|
|
803
|
+
<Show when={row.label}>
|
|
804
|
+
<dt>{row.label}</dt>
|
|
805
|
+
</Show>
|
|
806
|
+
<dd>{row.value}</dd>
|
|
807
|
+
</>
|
|
808
|
+
)}
|
|
809
|
+
</For>
|
|
810
|
+
<dt>Repository matching</dt>
|
|
811
|
+
<dd>{repositoryMatchingLabel(run())}</dd>
|
|
812
|
+
<Show when={run().assessment?.repositoryFindings.available}>
|
|
813
|
+
<dd class="gs-muted">
|
|
814
|
+
{run().assessment?.repositoryFindings.withMatchedFindingIds ??
|
|
815
|
+
'Unknown'}{' '}
|
|
816
|
+
records have matching finding IDs.{' '}
|
|
817
|
+
{run().assessment?.repositoryFindings.notConfirmedInLatestScan ??
|
|
818
|
+
'Unknown'}{' '}
|
|
819
|
+
were not confirmed in this scan. These are repository records, not
|
|
820
|
+
additional findings or deduplication progress.
|
|
821
|
+
</dd>
|
|
822
|
+
</Show>
|
|
823
|
+
</dl>
|
|
824
|
+
<For each={run().assessment?.notes ?? []}>
|
|
825
|
+
{(note) => <p class="gs-muted">{note}</p>}
|
|
826
|
+
</For>
|
|
827
|
+
<Show
|
|
828
|
+
when={run().skipped || run().assessment?.returnedFindings.informational}
|
|
829
|
+
>
|
|
830
|
+
<p class="gs-muted">
|
|
831
|
+
Informational and unclassified items are not shown in the four-severity
|
|
832
|
+
findings list. Review the full local SDK result for those items.
|
|
833
|
+
</p>
|
|
834
|
+
</Show>
|
|
835
|
+
</details>
|
|
836
|
+
</section>
|
|
837
|
+
</Show>
|
|
838
|
+
<details class="gs-activity" aria-label="Scan activity">
|
|
839
|
+
<summary>Activity and workers</summary>
|
|
840
|
+
<Show when={workerSummary(run())}>
|
|
841
|
+
<p class="gs-muted gs-small">{workerSummary(run())}</p>
|
|
842
|
+
</Show>
|
|
843
|
+
<Show when={dispatchLabel(run())}>
|
|
844
|
+
<p class="gs-muted gs-small">{dispatchLabel(run())}</p>
|
|
845
|
+
</Show>
|
|
846
|
+
<Show
|
|
847
|
+
when={activityRows(run()).length}
|
|
848
|
+
fallback={
|
|
849
|
+
<p class="gs-muted gs-small">
|
|
850
|
+
{run().status === 'running'
|
|
851
|
+
? 'Waiting for detailed SDK activity.'
|
|
852
|
+
: 'No detailed activity was recorded for this run.'}
|
|
853
|
+
</p>
|
|
854
|
+
}
|
|
855
|
+
>
|
|
856
|
+
<ul class="gs-worker-activity">
|
|
857
|
+
<For each={workerActivityRows(run()).slice(0, 4)}>
|
|
858
|
+
{(activity) => (
|
|
859
|
+
<li data-activity-id={activity.id}>
|
|
860
|
+
<div class="gs-activity-copy">
|
|
861
|
+
<span class="gs-activity-actor">
|
|
862
|
+
{activityActor(activity.worker)}
|
|
863
|
+
</span>
|
|
864
|
+
<span>{activity.label}</span>
|
|
865
|
+
</div>
|
|
866
|
+
<span class="gs-activity-state" data-status={activity.status}>
|
|
867
|
+
{activityStatusLabel(activity.status, run())}
|
|
868
|
+
</span>
|
|
869
|
+
</li>
|
|
870
|
+
)}
|
|
871
|
+
</For>
|
|
872
|
+
</ul>
|
|
873
|
+
</Show>
|
|
874
|
+
<Show when={activityRows(run()).length || run().events.length}>
|
|
875
|
+
<details class="gs-activity-history">
|
|
876
|
+
<summary>Activity history</summary>
|
|
877
|
+
<ol class="gs-activity-list">
|
|
878
|
+
<For each={activityRows(run())}>
|
|
879
|
+
{(activity) => (
|
|
880
|
+
<li>
|
|
881
|
+
<time dateTime={activity.updatedAt}>
|
|
882
|
+
{clockTime(activity.updatedAt)}
|
|
883
|
+
</time>
|
|
884
|
+
<span>
|
|
885
|
+
{activityActor(activity.worker)} · {activity.label} ·{' '}
|
|
886
|
+
{activityStatusLabel(activity.status, run())}
|
|
887
|
+
<Show when={activity.paths.length}>
|
|
888
|
+
<span class="gs-activity-paths">{activity.paths.join(', ')}</span>
|
|
889
|
+
</Show>
|
|
890
|
+
</span>
|
|
891
|
+
</li>
|
|
892
|
+
)}
|
|
893
|
+
</For>
|
|
894
|
+
</ol>
|
|
895
|
+
<Show when={run().events.length}>
|
|
896
|
+
<p class="gs-muted gs-small">Scan milestones · latest first</p>
|
|
897
|
+
<ol class="gs-activity-list">
|
|
898
|
+
<For each={[...run().events].reverse().slice(0, 20)}>
|
|
899
|
+
{(event) => (
|
|
900
|
+
<li>
|
|
901
|
+
<time dateTime={event.time}>{clockTime(event.time)}</time>
|
|
902
|
+
<span>{event.message}</span>
|
|
903
|
+
</li>
|
|
904
|
+
)}
|
|
905
|
+
</For>
|
|
906
|
+
</ol>
|
|
907
|
+
</Show>
|
|
908
|
+
</details>
|
|
909
|
+
</Show>
|
|
910
|
+
</details>
|
|
911
|
+
<details class="gs-advanced">
|
|
912
|
+
<summary>Usage and scan details</summary>
|
|
913
|
+
<dl class="gs-details-list">
|
|
914
|
+
<dt>Usage estimate</dt>
|
|
915
|
+
<dd>{usageLabel(run())}</dd>
|
|
916
|
+
<dd class="gs-muted">{usageContext(run())}</dd>
|
|
917
|
+
<Show when={usageRangeLabel(run())}>
|
|
918
|
+
<dt>Estimated range</dt>
|
|
919
|
+
<dd>{usageRangeLabel(run())}</dd>
|
|
920
|
+
</Show>
|
|
921
|
+
<Show when={run().telemetry?.measurement?.cost?.model}>
|
|
922
|
+
<dt>Pricing model</dt>
|
|
923
|
+
<dd>{run().telemetry?.measurement?.cost?.model}</dd>
|
|
924
|
+
</Show>
|
|
925
|
+
<Show when={run().telemetry?.measurement?.cost?.pricingAsOf}>
|
|
926
|
+
<dt>Pricing as of</dt>
|
|
927
|
+
<dd>{run().telemetry?.measurement?.cost?.pricingAsOf}</dd>
|
|
928
|
+
</Show>
|
|
929
|
+
<Show when={run().telemetry?.measurement?.usage}>
|
|
930
|
+
{(usage) => (
|
|
931
|
+
<>
|
|
932
|
+
<dt>Reported tokens</dt>
|
|
933
|
+
<dd>
|
|
934
|
+
{usage().inputTokens.toLocaleString()} input ·{' '}
|
|
935
|
+
{usage().cachedInputTokens.toLocaleString()} cached input ·{' '}
|
|
936
|
+
{usage().outputTokens.toLocaleString()} output
|
|
937
|
+
</dd>
|
|
938
|
+
</>
|
|
939
|
+
)}
|
|
940
|
+
</Show>
|
|
941
|
+
<Show when={run().deepProgress}>
|
|
942
|
+
<dt>Deep review</dt>
|
|
943
|
+
<dd>{deepProgressLabel(run())}</dd>
|
|
944
|
+
<dd class="gs-muted">
|
|
945
|
+
The cap is a maximum, not a target or completion percentage.
|
|
946
|
+
</dd>
|
|
947
|
+
</Show>
|
|
948
|
+
<Show when={reconnectLabel(run())}>
|
|
949
|
+
<dt>Connection</dt>
|
|
950
|
+
<dd>{reconnectLabel(run())}</dd>
|
|
951
|
+
</Show>
|
|
952
|
+
<dt>Repository</dt>
|
|
953
|
+
<dd>{run().repository}</dd>
|
|
954
|
+
<dt>Run ID</dt>
|
|
955
|
+
<dd class="gs-id">{run().runId}</dd>
|
|
956
|
+
<Show when={run().scanId}>
|
|
957
|
+
<dt>SDK scan ID</dt>
|
|
958
|
+
<dd class="gs-id">{run().scanId}</dd>
|
|
959
|
+
</Show>
|
|
960
|
+
<dt>Scope</dt>
|
|
961
|
+
<dd>{scanScope(run())}</dd>
|
|
962
|
+
<dt>Depth</dt>
|
|
963
|
+
<dd>
|
|
964
|
+
{run().mode ?? run().plan?.settings.scan.mode ?? 'Fixture pattern checks'}
|
|
965
|
+
</dd>
|
|
966
|
+
<Show when={run().model ?? run().plan?.settings.codex.model}>
|
|
967
|
+
<dt>Model</dt>
|
|
968
|
+
<dd>{run().model ?? run().plan?.settings.codex.model}</dd>
|
|
969
|
+
</Show>
|
|
970
|
+
<Show when={run().plan?.scanPrompt}>
|
|
971
|
+
<dt>Focus</dt>
|
|
972
|
+
<dd>{run().plan?.scanPrompt}</dd>
|
|
973
|
+
</Show>
|
|
974
|
+
<Show when={run().plan?.contextFiles.length}>
|
|
975
|
+
<dt>Supporting documents</dt>
|
|
976
|
+
<dd>
|
|
977
|
+
{run()
|
|
978
|
+
.plan?.contextFiles.map((f) => f.path)
|
|
979
|
+
.join(', ')}
|
|
980
|
+
</dd>
|
|
981
|
+
</Show>
|
|
982
|
+
</dl>
|
|
983
|
+
<Show when={run().archived}>
|
|
984
|
+
<p class="gs-muted">Restored from local history.</p>
|
|
985
|
+
</Show>
|
|
986
|
+
</details>
|
|
987
|
+
</article>
|
|
988
|
+
)}
|
|
989
|
+
</Show>
|
|
990
|
+
</Show>
|
|
991
|
+
<footer class="gs-footer">
|
|
992
|
+
{fixture()
|
|
993
|
+
? 'Fixture engine · not a security assessment'
|
|
994
|
+
: 'Codex Security · local results, OpenAI inference'}
|
|
995
|
+
<Show when={catalog().runs.length}>
|
|
996
|
+
{' '}
|
|
997
|
+
· Up to {catalog().maxConcurrent} concurrent scans per project
|
|
998
|
+
</Show>
|
|
999
|
+
</footer>
|
|
1000
|
+
</div>
|
|
1001
|
+
</section>
|
|
1002
|
+
);
|
|
1003
|
+
}
|