@azure-id/orc 1.4.1 → 1.5.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/CHANGELOG.md +3154 -2907
- package/README-id.md +39 -2
- package/README.md +647 -631
- package/bin/cli.js +42495 -36866
- package/bin/mockrun-catalog.js +4 -0
- package/bin/verify-contracts.js +4475 -4410
- package/bin/verify-package.js +21 -2
- package/bin/webui/api.js +1388 -1283
- package/bin/webui/app.html +9 -0
- package/bin/webui/css/04-motion.css +18 -0
- package/bin/webui/css/06-responsive.css +17 -1
- package/bin/webui/css/panels/hookui.css +220 -41
- package/bin/webui/css/panels/test.css +132 -0
- package/bin/webui/fixtures/index.js +23 -1
- package/bin/webui/fixtures/test.js +547 -0
- package/bin/webui/i18n/en/common.json +1 -0
- package/bin/webui/i18n/en/hookui.json +17 -3
- package/bin/webui/i18n/en/nav.json +1 -0
- package/bin/webui/i18n/en/overview.json +4 -0
- package/bin/webui/i18n/en/test.json +141 -0
- package/bin/webui/i18n/en/tour.json +1 -1
- package/bin/webui/i18n/id/common.json +1 -0
- package/bin/webui/i18n/id/hookui.json +17 -3
- package/bin/webui/i18n/id/nav.json +1 -0
- package/bin/webui/i18n/id/overview.json +4 -0
- package/bin/webui/i18n/id/test.json +141 -0
- package/bin/webui/i18n/id/tour.json +1 -1
- package/bin/webui/js/01-i18n.js +1 -0
- package/bin/webui/js/panels/challenge.js +8 -3
- package/bin/webui/js/panels/hookui.js +586 -208
- package/bin/webui/js/panels/overview.js +10 -0
- package/bin/webui/js/panels/test.js +733 -0
- package/bin/webui/serve.js +572 -557
- package/mock-run/orc-test.md +327 -0
- package/package.json +1 -1
- package/templates/agents/MODEL-MAPPING.md +2 -0
- package/templates/agents/orc-test-designer-opus-5-high.md +158 -0
- package/templates/agents/orc-test-interpreter-opus-5-low.md +130 -0
- package/templates/commands/orc-test.md +66 -0
- package/templates/hooks/README.md +8 -1
- package/templates/hooks/orc-statusline.js +15 -4
- package/templates/hooks/orc-subagent-line.js +5 -2
- package/templates/skills/_shared/extra-dispatch.md +1346 -1337
- package/templates/skills/_shared/live-target.md +188 -0
- package/templates/skills/_shared/phases/trace.md +2 -2
- package/templates/skills/_shared/wait.md +241 -240
- package/templates/skills/orc-test/SKILL.md +233 -0
|
@@ -0,0 +1,733 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/* panels/test.js — orc ui client
|
|
3
|
+
/orc-test: the lane that RUNS the test. What was measured, what was NOT, and
|
|
4
|
+
with whose permission.
|
|
5
|
+
|
|
6
|
+
FIVE TABS, on the Knowledge/Extra precedent: Targets · Surface · Cases ·
|
|
7
|
+
Runs · Findings. The header strip and the findings caution stay OUTSIDE the
|
|
8
|
+
tabs — a caution you must hunt for is a caution nobody reads — and the tab
|
|
9
|
+
survives a re-render (`KN_TAB`'s rule).
|
|
10
|
+
|
|
11
|
+
THE PANEL DERIVES NOTHING. Not a state word, not a severity, not an OWASP
|
|
12
|
+
id, not a tier, not a verdict, not the code-vs-live diff. `orc test show
|
|
13
|
+
--json` is the whole computed object and this file renders it. A test greps
|
|
14
|
+
this panel for the literals it must not own.
|
|
15
|
+
|
|
16
|
+
TWO LINES THIS PANEL DOES NOT CROSS.
|
|
17
|
+
· A FREE ACTION GETS A BUTTON — `surface`, `env`, `report`. Every one costs
|
|
18
|
+
zero model tokens and every one is a real CLI command.
|
|
19
|
+
· A PAID ACTION GETS A COPY-ABLE COMMAND. So does anything that SENDS
|
|
20
|
+
TRAFFIC: `orc test run` is where the requests are, and a page that can
|
|
21
|
+
start a scan against a live host is a page that can start one by
|
|
22
|
+
accident.
|
|
23
|
+
|
|
24
|
+
And the reads are reads in the strict sense: opening this page re-scans
|
|
25
|
+
nothing and probes nothing. `orc test env` is the one probe here and it is
|
|
26
|
+
behind a button you press, the `orc extra ping` shape.
|
|
27
|
+
|
|
28
|
+
Loaded by app.html in the order its numeric prefix names. Classic
|
|
29
|
+
script, no import/export: an ES module import carries no query string,
|
|
30
|
+
and every static request here needs the per-launch session token. */
|
|
31
|
+
|
|
32
|
+
/* ==================================================================== TEST == */
|
|
33
|
+
|
|
34
|
+
// Which tab was open, and which run was selected, so a write that re-renders
|
|
35
|
+
// the panel does not throw the user back to the first of either.
|
|
36
|
+
let TEST_TAB = "targets";
|
|
37
|
+
let TEST_SLUG = null;
|
|
38
|
+
// The last environment reading, per slug, held in this page's memory only. A
|
|
39
|
+
// health state is NOT stored on disk — it is computed fresh on every read (the
|
|
40
|
+
// computeWikiFreshness rule) — so the panel must never render a stale word as
|
|
41
|
+
// if it were current, and must never persist one.
|
|
42
|
+
const TEST_ENV_SEEN = {};
|
|
43
|
+
|
|
44
|
+
// The kind map for the CLI's own state words. The WORDS are the CLI's and are
|
|
45
|
+
// rendered as they come; only the colour is ours.
|
|
46
|
+
const TEST_STATE_KIND = { RED: "bad", UNREADABLE: "bad", OBSERVED: "ok", PARTIAL: "warn" };
|
|
47
|
+
const TEST_VERDICT_KIND = { pass: "ok", fail: "bad", unknown: "warn" };
|
|
48
|
+
const TEST_ENV_KIND = { ready: "ok", starting: "warn", down: "warn", unhealthy: "bad", absent: "bad" };
|
|
49
|
+
const TEST_SEC_KIND = { FOUND: "bad", "observed-clean": "ok", "partly-observed": "warn", unchecked: "warn", "not-observed": "warn", "not-run": "" };
|
|
50
|
+
const TEST_SEV_KIND = { critical: "bad", high: "bad", medium: "warn", low: "" };
|
|
51
|
+
const TEST_TOOL_KIND = { ready: "ok", outdated: "warn", unauthenticated: "warn", absent: "bad" };
|
|
52
|
+
|
|
53
|
+
PANELS.test = function (host) {
|
|
54
|
+
head(host, t("test.title"), t("test.sub"));
|
|
55
|
+
const body = el("div", "stack");
|
|
56
|
+
host.append(body);
|
|
57
|
+
renderTest(body);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
async function renderTest(body) {
|
|
61
|
+
body.replaceChildren(skeleton(6));
|
|
62
|
+
const [listRes, uiRes] = await Promise.all([
|
|
63
|
+
read("/api/test").catch((e) => ({ data: null, error: e })),
|
|
64
|
+
read("/api/test/ui").catch((e) => ({ data: null, error: e })),
|
|
65
|
+
]);
|
|
66
|
+
if (!listRes.data) {
|
|
67
|
+
body.replaceChildren(failBox(listRes.error));
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const list = listRes.data;
|
|
71
|
+
const tools = (uiRes.data && uiRes.data.ui) || null;
|
|
72
|
+
|
|
73
|
+
if (!list.runs || !list.runs.length) {
|
|
74
|
+
const out = frag();
|
|
75
|
+
const c = card(t("test.title"));
|
|
76
|
+
c.append(empty(t("test.none"), t("test.noneHint")));
|
|
77
|
+
c.append(
|
|
78
|
+
laneCommand(
|
|
79
|
+
'orc test init <slug> --kind be --env local --start auto --destructive deny --reason "<why>"',
|
|
80
|
+
t("test.initCmd"),
|
|
81
|
+
t("common.runInTerminal")
|
|
82
|
+
)
|
|
83
|
+
);
|
|
84
|
+
out.append(c);
|
|
85
|
+
if (tools) out.append(testToolsCard(tools));
|
|
86
|
+
body.replaceChildren(out);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// The selected run. An unknown remembered slug falls back to the first row
|
|
91
|
+
// rather than rendering nothing.
|
|
92
|
+
const slugs = list.runs.map((r) => r.slug);
|
|
93
|
+
if (!TEST_SLUG || slugs.indexOf(TEST_SLUG) === -1) TEST_SLUG = slugs[0];
|
|
94
|
+
|
|
95
|
+
const showRes = await read("/api/test/one?slug=" + encodeURIComponent(TEST_SLUG)).catch((e) => ({ data: null, error: e }));
|
|
96
|
+
const d = showRes.data;
|
|
97
|
+
|
|
98
|
+
const out = frag();
|
|
99
|
+
out.append(testPicker(list, body));
|
|
100
|
+
|
|
101
|
+
// An UNREADABLE ledger is a ROW, never a crash and never a silent gap. It
|
|
102
|
+
// never reaches a tab, because there is nothing measured behind it.
|
|
103
|
+
if (!d || d.state === "UNREADABLE") {
|
|
104
|
+
const c = card(TEST_SLUG);
|
|
105
|
+
c.append(chip(String((d && d.state) || "UNREADABLE"), "bad"));
|
|
106
|
+
c.append(el("div", "note", t("test.target.unreadable")));
|
|
107
|
+
if (d && d.error) c.append(el("pre", "block wrap", esc(d.error)));
|
|
108
|
+
out.append(c);
|
|
109
|
+
if (tools) out.append(testToolsCard(tools));
|
|
110
|
+
body.replaceChildren(out);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// THE HEADER STRIP and THE CAUTION, above the tabs and on every one of them.
|
|
115
|
+
out.append(testStrip(d));
|
|
116
|
+
const caution = testCaution(d);
|
|
117
|
+
if (caution) out.append(caution);
|
|
118
|
+
|
|
119
|
+
const tabs = el("div", "tabs");
|
|
120
|
+
const pane = el("div", "tab-pane stack");
|
|
121
|
+
const views = {
|
|
122
|
+
targets: () => testTargetsTab(d, tools, body),
|
|
123
|
+
surface: () => testSurfaceTab(d, body),
|
|
124
|
+
cases: () => testCasesTab(d),
|
|
125
|
+
runs: () => testRunsTab(d),
|
|
126
|
+
findings: () => testFindingsTab(d, body),
|
|
127
|
+
};
|
|
128
|
+
const select = (which) => {
|
|
129
|
+
TEST_TAB = which;
|
|
130
|
+
for (const b of tabs.children) b.setAttribute("aria-selected", String(b.dataset.tab === which));
|
|
131
|
+
pane.replaceChildren(views[which]());
|
|
132
|
+
};
|
|
133
|
+
// Keys are written out in full, never assembled from the tab id — a key built
|
|
134
|
+
// from a fragment is invisible to every check that looks for one.
|
|
135
|
+
for (const [which, label] of [
|
|
136
|
+
["targets", t("test.tab.targets")],
|
|
137
|
+
["surface", t("test.tab.surface")],
|
|
138
|
+
["cases", t("test.tab.cases")],
|
|
139
|
+
["runs", t("test.tab.runs")],
|
|
140
|
+
["findings", t("test.tab.findings")],
|
|
141
|
+
]) {
|
|
142
|
+
const b = el("button", null, label);
|
|
143
|
+
b.type = "button";
|
|
144
|
+
b.dataset.tab = which;
|
|
145
|
+
b.addEventListener("click", () => select(which));
|
|
146
|
+
tabs.append(b);
|
|
147
|
+
}
|
|
148
|
+
out.append(tabs, pane);
|
|
149
|
+
body.replaceChildren(out);
|
|
150
|
+
select(views[TEST_TAB] ? TEST_TAB : "targets");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/* THE RUN PICKER. Every run keeps its row, including the UNREADABLE one: a
|
|
154
|
+
listing that drops it is a listing that hides the one run that needs
|
|
155
|
+
attention. */
|
|
156
|
+
function testPicker(list, body) {
|
|
157
|
+
const c = card(t("test.pick"));
|
|
158
|
+
const bar = el("div", "toolbar");
|
|
159
|
+
const sel = el("select", "text-input");
|
|
160
|
+
for (const r of list.runs) {
|
|
161
|
+
const o = el("option", null, r.slug + " — " + r.state);
|
|
162
|
+
o.value = r.slug;
|
|
163
|
+
if (r.slug === TEST_SLUG) o.selected = true;
|
|
164
|
+
sel.append(o);
|
|
165
|
+
}
|
|
166
|
+
sel.addEventListener("change", () => {
|
|
167
|
+
TEST_SLUG = sel.value;
|
|
168
|
+
renderTest(body);
|
|
169
|
+
});
|
|
170
|
+
bar.append(sel);
|
|
171
|
+
bar.append(el("span", "toolbar-result", list.dir));
|
|
172
|
+
c.append(bar);
|
|
173
|
+
c.append(el("div", "note", t("test.pickHint")));
|
|
174
|
+
return c;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/* THE HEADER STRIP. Six values, all of them the CLI's. */
|
|
178
|
+
function testStrip(d) {
|
|
179
|
+
const strip = el("div", "ts-strip");
|
|
180
|
+
const item = (label, value, kind) => {
|
|
181
|
+
const box = el("div", "ts-strip-item" + (kind ? " ts-" + kind : ""));
|
|
182
|
+
box.append(el("span", "ts-strip-value", value));
|
|
183
|
+
box.append(el("span", "ts-strip-label", label));
|
|
184
|
+
strip.append(box);
|
|
185
|
+
};
|
|
186
|
+
const counts = d.counts || {};
|
|
187
|
+
item(t("test.strip.state"), d.state, TEST_STATE_KIND[d.state] || "");
|
|
188
|
+
item(t("test.strip.cases"), String((d.cases || []).length));
|
|
189
|
+
item(t("test.strip.runs"), String((d.runs || []).length));
|
|
190
|
+
const observed = (d.findings || []).filter((f) => f.observed).length;
|
|
191
|
+
item(t("test.strip.findings"), String((d.findings || []).length), observed ? "bad" : "");
|
|
192
|
+
item(t("test.strip.observed"), String(observed), observed ? "bad" : "");
|
|
193
|
+
// The tier word is the CLI's. Never a friendlier synonym.
|
|
194
|
+
item(t("test.strip.security"), (d.security && d.security.tier) || "—", d.security && d.security.found ? "bad" : "");
|
|
195
|
+
return strip;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/* THE CAUTION, outside the tabs. It fires only on something the run actually
|
|
199
|
+
MEASURED — a red case or an OBSERVED finding — and it routes to the tab that
|
|
200
|
+
can show the evidence. */
|
|
201
|
+
function testCaution(d) {
|
|
202
|
+
const red = (d.counts && d.counts.fail) || 0;
|
|
203
|
+
const observed = (d.findings || []).filter((f) => f.observed).length;
|
|
204
|
+
if (!red && !observed) return null;
|
|
205
|
+
const box = el("div", "ts-caution");
|
|
206
|
+
const line = el("div", "ts-caution-line");
|
|
207
|
+
line.append(chip(d.state, TEST_STATE_KIND[d.state] || "bad"));
|
|
208
|
+
line.append(el("span", null, d.where));
|
|
209
|
+
box.append(line);
|
|
210
|
+
box.append(el("div", "note", t("test.findings.evidenceNote")));
|
|
211
|
+
return box;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/* ------------------------------------------------------------- 1. TARGETS -- */
|
|
215
|
+
|
|
216
|
+
function testTargetsTab(d, tools, body) {
|
|
217
|
+
const out = frag();
|
|
218
|
+
out.append(el("div", "note", t("test.free")));
|
|
219
|
+
|
|
220
|
+
const c = card(t("test.target.title"));
|
|
221
|
+
const g = d.target || {};
|
|
222
|
+
c.append(
|
|
223
|
+
kvList([
|
|
224
|
+
[t("test.target.kind"), g.kind],
|
|
225
|
+
[t("test.target.env"), g.env],
|
|
226
|
+
[t("test.target.baseUrl"), g.base_url],
|
|
227
|
+
[t("test.target.origin"), g.origin],
|
|
228
|
+
])
|
|
229
|
+
);
|
|
230
|
+
// THE AUTHORIZATION STATEMENT AND THE DESTRUCTIVE DECISION RENDER ALWAYS, and
|
|
231
|
+
// verbatim. A statement nobody filled in is a STATE, said out loud — never an
|
|
232
|
+
// empty row and never a cheerful default.
|
|
233
|
+
const auth = el("div", "ts-auth" + (d.authorized ? "" : " ts-auth-empty"));
|
|
234
|
+
auth.append(el("div", "ts-auth-label", t("test.target.authorized")));
|
|
235
|
+
auth.append(el("div", "ts-auth-value", d.authorized || t("test.target.authorizedEmpty")));
|
|
236
|
+
auth.append(el("div", "note", t("test.target.authorizedNote")));
|
|
237
|
+
c.append(auth);
|
|
238
|
+
const de = d.destructive || {};
|
|
239
|
+
const dbox = el("div", "row-actions");
|
|
240
|
+
dbox.append(el("span", "ts-label", t("test.target.destructive")));
|
|
241
|
+
dbox.append(chip(de.mode || "—", de.mode === "allow" ? "warn" : ""));
|
|
242
|
+
c.append(dbox);
|
|
243
|
+
if (de.reason) c.append(kvList([[t("test.target.destructiveReason"), de.reason]]));
|
|
244
|
+
c.append(el("div", "note", t("test.target.frozen")));
|
|
245
|
+
if (g.env === "remote") c.append(el("div", "note", t("test.target.remoteNote")));
|
|
246
|
+
out.append(c);
|
|
247
|
+
|
|
248
|
+
// Identities. The one-identity sentence is stated wherever the count is
|
|
249
|
+
// shown, because it decides what the security tier can honestly report.
|
|
250
|
+
const ic = card(t("test.target.identities"));
|
|
251
|
+
if (!(d.identities || []).length) ic.append(empty(t("test.target.oneIdentity")));
|
|
252
|
+
for (const i of d.identities || []) {
|
|
253
|
+
const row = el("div", "row-actions");
|
|
254
|
+
row.append(el("strong", null, i.name));
|
|
255
|
+
row.append(chip(i.role, ""));
|
|
256
|
+
row.append(chip(i.source, ""));
|
|
257
|
+
if (i.verified_at) row.append(chip(i.verified_at, "ok"));
|
|
258
|
+
ic.append(row);
|
|
259
|
+
}
|
|
260
|
+
if ((d.identities || []).length && !d.two_identities) ic.append(el("div", "note", t("test.target.oneIdentity")));
|
|
261
|
+
out.append(ic);
|
|
262
|
+
|
|
263
|
+
out.append(testEnvCard(d, body));
|
|
264
|
+
if (tools) out.append(testToolsCard(tools));
|
|
265
|
+
return out;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/* THE ENVIRONMENT. A button, and the outcome is the state. It is not fetched on
|
|
269
|
+
open: a health state is not stored, so rendering one on open would mean
|
|
270
|
+
probing the target every time somebody looks at a page. */
|
|
271
|
+
function testEnvCard(d, body) {
|
|
272
|
+
const c = card(t("test.env.title"));
|
|
273
|
+
const seen = TEST_ENV_SEEN[d.slug] || null;
|
|
274
|
+
|
|
275
|
+
const bar = el("div", "row-actions");
|
|
276
|
+
const b = el("button", "btn btn-sm", t("test.env.check"));
|
|
277
|
+
b.type = "button";
|
|
278
|
+
b.addEventListener("click", async () => {
|
|
279
|
+
b.disabled = true;
|
|
280
|
+
setBusy(true);
|
|
281
|
+
try {
|
|
282
|
+
const r = await post("/api/test/env", { slug: d.slug });
|
|
283
|
+
TEST_ENV_SEEN[d.slug] = r.data || { state: null, refused: r };
|
|
284
|
+
} catch (e) {
|
|
285
|
+
TEST_ENV_SEEN[d.slug] = { error: (e && e.message) || String(e) };
|
|
286
|
+
} finally {
|
|
287
|
+
setBusy(false);
|
|
288
|
+
b.disabled = false;
|
|
289
|
+
renderTest(body);
|
|
290
|
+
}
|
|
291
|
+
});
|
|
292
|
+
bar.append(b);
|
|
293
|
+
c.append(bar);
|
|
294
|
+
c.append(el("div", "note", t("test.env.checkWhy")));
|
|
295
|
+
|
|
296
|
+
if (!seen) {
|
|
297
|
+
c.append(el("div", "note", t("test.env.notStored")));
|
|
298
|
+
return c;
|
|
299
|
+
}
|
|
300
|
+
if (seen.error) {
|
|
301
|
+
c.append(el("pre", "block wrap", esc(seen.error)));
|
|
302
|
+
return c;
|
|
303
|
+
}
|
|
304
|
+
// The REMOTE refusal. ORC will not start or stop anything on a host you do
|
|
305
|
+
// not own, and it says so by name rather than rendering an empty card.
|
|
306
|
+
if (seen.ok === false) {
|
|
307
|
+
// The CLI's own reason word, and NO fallback: a refusal ORC could not name
|
|
308
|
+
// is a refusal this panel must not name for it. With no reason the hint
|
|
309
|
+
// stands alone rather than under a word nobody computed.
|
|
310
|
+
if (seen.reason) c.append(chip(String(seen.reason), "warn"));
|
|
311
|
+
c.append(el("div", "note", esc(seen.hint || "")));
|
|
312
|
+
return c;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const row = el("div", "row-actions");
|
|
316
|
+
row.append(chip(String(seen.state), TEST_ENV_KIND[seen.state] || ""));
|
|
317
|
+
if (seen.next && seen.next.action) row.append(el("span", null, seen.next.action));
|
|
318
|
+
c.append(row);
|
|
319
|
+
if (seen.next && seen.next.why) c.append(el("div", "note", esc(seen.next.why)));
|
|
320
|
+
if (seen.state === "unhealthy") c.append(el("div", "ts-handback", t("test.env.handBack")));
|
|
321
|
+
|
|
322
|
+
const rows = [];
|
|
323
|
+
if (seen.detected && seen.detected.command) rows.push([t("test.env.detected"), seen.detected.command + " (" + seen.detected.rung + ")"]);
|
|
324
|
+
if (seen.process) rows.push([t("test.env.process"), "pid " + seen.process.pid + (seen.process.alive ? "" : " — not alive")]);
|
|
325
|
+
if (rows.length) c.append(kvList(rows));
|
|
326
|
+
|
|
327
|
+
if (seen.health && (seen.health.tried || []).length) {
|
|
328
|
+
const h = collapsible({
|
|
329
|
+
title: t("test.env.health"),
|
|
330
|
+
count: String(seen.health.tried.length),
|
|
331
|
+
desc: seen.health.why || "",
|
|
332
|
+
collapsed: true,
|
|
333
|
+
content: (() => {
|
|
334
|
+
const box = el("div", "stack stack-sm");
|
|
335
|
+
for (const x of seen.health.tried) {
|
|
336
|
+
const r2 = el("div", "row-actions");
|
|
337
|
+
r2.append(el("code", null, x.path));
|
|
338
|
+
r2.append(chip(x.status === null ? String(x.reason || "—") : String(x.status), x.status && x.status < 400 ? "ok" : "warn"));
|
|
339
|
+
box.append(r2);
|
|
340
|
+
}
|
|
341
|
+
return box;
|
|
342
|
+
})(),
|
|
343
|
+
});
|
|
344
|
+
c.append(h);
|
|
345
|
+
}
|
|
346
|
+
if (!seen.detected || !seen.detected.command) {
|
|
347
|
+
if ((seen.looked_for || []).length) c.append(kvList([[t("test.env.lookedFor"), seen.looked_for.join(", ")]]));
|
|
348
|
+
}
|
|
349
|
+
if (seen.env_vars && (seen.env_vars.missing || []).length) {
|
|
350
|
+
c.append(kvList([[t("test.env.vars"), seen.env_vars.missing.join(", ")]]));
|
|
351
|
+
c.append(el("div", "note", t("test.env.varsNote")));
|
|
352
|
+
}
|
|
353
|
+
if ((seen.log_tail || []).length) {
|
|
354
|
+
c.append(el("div", "ts-label", t("test.env.logs")));
|
|
355
|
+
c.append(el("pre", "block wrap", seen.log_tail.join("\n")));
|
|
356
|
+
}
|
|
357
|
+
return c;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/* THE FRONT-END DRIVER. A tool can simply be absent, and that is a STATE before
|
|
361
|
+
it is a failure. ORC NAMES the install command and never runs it. */
|
|
362
|
+
function testToolsCard(tools) {
|
|
363
|
+
const c = card(t("test.ui.title"));
|
|
364
|
+
const row = el("div", "row-actions");
|
|
365
|
+
row.append(chip(String(tools.state), TEST_TOOL_KIND[tools.state] || ""));
|
|
366
|
+
c.append(row);
|
|
367
|
+
if (tools.why) c.append(el("div", "note", esc(tools.why)));
|
|
368
|
+
if (tools.disabled) c.append(el("div", "note", t("test.ui.off")));
|
|
369
|
+
const rows = [];
|
|
370
|
+
if (tools.bin) rows.push([t("test.ui.binary"), tools.bin]);
|
|
371
|
+
if (tools.from) rows.push([t("test.ui.from"), tools.from]);
|
|
372
|
+
if (tools.version) rows.push([t("test.ui.version"), tools.version]);
|
|
373
|
+
if (tools.min_version) rows.push([t("test.ui.minVersion"), tools.min_version]);
|
|
374
|
+
if ((tools.browsers || []).length) rows.push([t("test.ui.browsers"), tools.browsers.join(", ")]);
|
|
375
|
+
if (rows.length) c.append(kvList(rows));
|
|
376
|
+
if (!tools.disabled && tools.state !== "ready" && tools.next) {
|
|
377
|
+
c.append(laneCommand(tools.next, t("test.ui.installNote"), t("common.runInTerminal")));
|
|
378
|
+
// `no_install_alternative: null` MEANS there is none — never that ORC
|
|
379
|
+
// forgot to look.
|
|
380
|
+
if (tools.no_install_alternative === null) c.append(el("div", "note", t("test.ui.noAlternative")));
|
|
381
|
+
}
|
|
382
|
+
c.append(el("div", "note", t("test.ui.fresh")));
|
|
383
|
+
return c;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/* ------------------------------------------------------------- 2. SURFACE -- */
|
|
387
|
+
|
|
388
|
+
function testSurfaceTab(d, body) {
|
|
389
|
+
const out = frag();
|
|
390
|
+
const s = d.surface;
|
|
391
|
+
|
|
392
|
+
const bar = card(t("test.surface.take"));
|
|
393
|
+
const row = el("div", "row-actions");
|
|
394
|
+
const b = el("button", "btn btn-sm", t("test.surface.take"));
|
|
395
|
+
b.type = "button";
|
|
396
|
+
b.addEventListener("click", async () => {
|
|
397
|
+
b.disabled = true;
|
|
398
|
+
setBusy(true);
|
|
399
|
+
try {
|
|
400
|
+
await post("/api/test/surface", { slug: d.slug });
|
|
401
|
+
} catch (e) {
|
|
402
|
+
toast(String((e && e.message) || e), "bad");
|
|
403
|
+
} finally {
|
|
404
|
+
setBusy(false);
|
|
405
|
+
renderTest(body);
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
row.append(b);
|
|
409
|
+
bar.append(row);
|
|
410
|
+
bar.append(el("div", "note", t("test.surface.takeWhy")));
|
|
411
|
+
out.append(bar);
|
|
412
|
+
|
|
413
|
+
if (!s) {
|
|
414
|
+
out.append(empty(t("test.surface.none"), t("test.surface.takeWhy")));
|
|
415
|
+
return out;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const sum = card(t("test.surface.title"));
|
|
419
|
+
sum.append(
|
|
420
|
+
kvList([
|
|
421
|
+
[t("test.surface.specDisk"), s.spec_on_disk ? s.spec_on_disk.file + (s.spec_on_disk.parsed ? "" : " — " + s.spec_on_disk.why) : "—"],
|
|
422
|
+
[t("test.surface.specTarget"), s.spec_at_target && s.spec_at_target.found ? s.spec_at_target.found : "—"],
|
|
423
|
+
[t("test.surface.frameworks"), (s.frameworks || []).join(", ") || "—"],
|
|
424
|
+
[t("test.surface.files"), String(s.files_scanned)],
|
|
425
|
+
[t("test.surface.routes"), String((s.routes || []).length)],
|
|
426
|
+
[t("test.surface.mounts"), (s.mounts || []).map((m) => m.prefix).join(", ") || "—"],
|
|
427
|
+
])
|
|
428
|
+
);
|
|
429
|
+
if ((s.mounts || []).length) sum.append(el("div", "note", t("test.surface.mountsNote")));
|
|
430
|
+
out.append(sum);
|
|
431
|
+
|
|
432
|
+
// THE CODE-VS-LIVE DIFF. A `null` source means NOT MEASURED and carries its
|
|
433
|
+
// own reason — the one thing that must never render as "no shadow APIs".
|
|
434
|
+
const dc = card(t("test.surface.diff"));
|
|
435
|
+
if (s.diff && s.diff.source) {
|
|
436
|
+
const r1 = el("div", "row-actions");
|
|
437
|
+
r1.append(el("span", "ts-label", t("test.surface.codeOnly")));
|
|
438
|
+
r1.append(chip(String(s.diff.code_only.length), s.diff.code_only.length ? "warn" : ""));
|
|
439
|
+
dc.append(r1);
|
|
440
|
+
for (const x of s.diff.code_only) dc.append(el("div", "ts-route-min", x.key + (x.file ? " " + x.file + (x.line ? ":" + x.line : "") : "")));
|
|
441
|
+
const r2 = el("div", "row-actions");
|
|
442
|
+
r2.append(el("span", "ts-label", t("test.surface.liveOnly")));
|
|
443
|
+
r2.append(chip(String(s.diff.live_only.length), s.diff.live_only.length ? "bad" : ""));
|
|
444
|
+
dc.append(r2);
|
|
445
|
+
for (const x of s.diff.live_only) dc.append(el("div", "ts-route-min ts-bad", x.key));
|
|
446
|
+
dc.append(el("div", "note", esc(s.diff.note)));
|
|
447
|
+
} else {
|
|
448
|
+
dc.append(chip(t("test.surface.notMeasured"), "warn"));
|
|
449
|
+
dc.append(el("div", "note", esc((s.diff && s.diff.why) || "")));
|
|
450
|
+
}
|
|
451
|
+
out.append(dc);
|
|
452
|
+
|
|
453
|
+
const rc = card(t("test.surface.routes"));
|
|
454
|
+
for (const r of s.routes || []) {
|
|
455
|
+
const row2 = el("div", "ts-route");
|
|
456
|
+
row2.append(el("span", "ts-method", r.method));
|
|
457
|
+
row2.append(el("span", "ts-path", r.path));
|
|
458
|
+
row2.append(chip(r.in_code ? t("test.surface.inCode") : t("test.surface.notInCode"), r.in_code ? "" : "bad"));
|
|
459
|
+
// `live: null` is "we did not look", which is a different fact from "it is
|
|
460
|
+
// not there" — and only one of them is a finding.
|
|
461
|
+
row2.append(chip(r.live === null ? t("test.surface.liveUnknown") : r.live ? t("test.surface.liveYes") : t("test.surface.notLive"), r.live === null ? "" : r.live ? "ok" : "warn"));
|
|
462
|
+
const where = el("span", "ts-where", r.file ? r.file + (r.line ? ":" + r.line : "") : r.source);
|
|
463
|
+
row2.append(where);
|
|
464
|
+
rc.append(row2);
|
|
465
|
+
}
|
|
466
|
+
out.append(rc);
|
|
467
|
+
|
|
468
|
+
if ((s.unresolved || []).length) {
|
|
469
|
+
const uc = card(t("test.surface.unresolved"));
|
|
470
|
+
for (const u of s.unresolved) uc.append(el("div", "ts-route-min", u.file + ":" + u.line + " " + u.text));
|
|
471
|
+
uc.append(el("div", "note", t("test.surface.unresolvedNote")));
|
|
472
|
+
out.append(uc);
|
|
473
|
+
}
|
|
474
|
+
return out;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/* --------------------------------------------------------------- 3. CASES -- */
|
|
478
|
+
|
|
479
|
+
function testCasesTab(d) {
|
|
480
|
+
const out = frag();
|
|
481
|
+
const cases = d.cases || [];
|
|
482
|
+
|
|
483
|
+
const head2 = card(t("test.cases.budget"));
|
|
484
|
+
head2.append(el("div", "note", t("test.cases.derived")));
|
|
485
|
+
if (d.case_budget) {
|
|
486
|
+
head2.append(kvList([[t("test.cases.budget"), String(d.case_budget.budget)]]));
|
|
487
|
+
if (d.case_budget.reached) {
|
|
488
|
+
head2.append(el("div", "note", t("test.cases.budgetReached")));
|
|
489
|
+
if ((d.case_budget.unfinished || []).length)
|
|
490
|
+
head2.append(kvList([[t("test.cases.unfinished"), d.case_budget.unfinished.join(", ")]]));
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
const gaps = cases.filter((c) => (c.gaps || []).length).length;
|
|
494
|
+
if (gaps) {
|
|
495
|
+
const gr = el("div", "row-actions");
|
|
496
|
+
gr.append(chip(gaps + " " + t("test.cases.gap"), "warn"));
|
|
497
|
+
head2.append(gr);
|
|
498
|
+
head2.append(el("div", "note", t("test.cases.gapNote")));
|
|
499
|
+
}
|
|
500
|
+
head2.append(laneCommand("orc test case derive " + d.slug, t("test.paidWhy"), t("common.runInTerminal")));
|
|
501
|
+
out.append(head2);
|
|
502
|
+
|
|
503
|
+
if (!cases.length) {
|
|
504
|
+
out.append(empty(t("test.cases.none"), t("test.cases.derived")));
|
|
505
|
+
} else {
|
|
506
|
+
const lc = card(t("test.cases.title"));
|
|
507
|
+
for (const c of cases) {
|
|
508
|
+
const row = el("div", "ts-case");
|
|
509
|
+
row.append(el("span", "ts-case-id", c.id));
|
|
510
|
+
row.append(chip(c.tier, ""));
|
|
511
|
+
row.append(chip(c.verdict || t("test.cases.notRun"), TEST_VERDICT_KIND[c.verdict] || ""));
|
|
512
|
+
const mid = el("div", "ts-case-mid");
|
|
513
|
+
mid.append(el("div", "ts-case-target", c.target));
|
|
514
|
+
mid.append(el("div", "ts-case-why", c.why));
|
|
515
|
+
if (c.verdict_why) mid.append(el("div", "ts-case-why", c.verdict_why));
|
|
516
|
+
if ((c.gaps || []).length) for (const g of c.gaps) mid.append(el("div", "ts-case-gap", g));
|
|
517
|
+
// The EVIDENCE PATH, never the evidence BODY. A captured response body is
|
|
518
|
+
// the most sensitive thing this lane writes to disk, and it stays on the
|
|
519
|
+
// disk: this panel names where it is and does not open it.
|
|
520
|
+
if (c.evidence) mid.append(el("div", "ts-case-ev", t("test.cases.evidence") + ": " + c.evidence));
|
|
521
|
+
row.append(mid);
|
|
522
|
+
const tail = el("div", "ts-case-tail");
|
|
523
|
+
if (c.owasp) tail.append(chip(c.owasp, ""));
|
|
524
|
+
if (c.identity) tail.append(chip(t("test.cases.identity") + " " + c.identity, ""));
|
|
525
|
+
if (c.mutates) tail.append(chip(t("test.cases.mutates"), "warn"));
|
|
526
|
+
row.append(tail);
|
|
527
|
+
lc.append(row);
|
|
528
|
+
}
|
|
529
|
+
out.append(lc);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
out.append(testSecurityCard(d));
|
|
533
|
+
return out;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/* THE CLOSED OWASP SET. Every one of the ten rows renders, in every state:
|
|
537
|
+
`unchecked` KEEPS ITS SLOT with its reason, never becomes a pass, and never
|
|
538
|
+
raises the exit code. The ids, the names, the state words and the severities
|
|
539
|
+
are all the CLI's. */
|
|
540
|
+
function testSecurityCard(d) {
|
|
541
|
+
const sec = d.security || {};
|
|
542
|
+
const c = card(t("test.security.title"));
|
|
543
|
+
const bar = el("div", "row-actions");
|
|
544
|
+
bar.append(el("span", "ts-label", t("test.security.tier")));
|
|
545
|
+
bar.append(chip(String(sec.tier), sec.tier === "off" ? "warn" : ""));
|
|
546
|
+
if (sec.tier_source && sec.tier_source.source) bar.append(el("span", "ts-where", sec.tier_source.source));
|
|
547
|
+
c.append(bar);
|
|
548
|
+
c.append(el("div", "note", t("test.security.tierDerived")));
|
|
549
|
+
for (const note of sec.notes || []) c.append(el("div", "ts-handback", esc(note)));
|
|
550
|
+
|
|
551
|
+
for (const r of sec.rows || []) {
|
|
552
|
+
const row = el("div", "ts-owasp");
|
|
553
|
+
row.append(el("span", "ts-owasp-id", r.owasp));
|
|
554
|
+
row.append(chip(r.state, TEST_SEC_KIND[r.state] || ""));
|
|
555
|
+
const mid = el("div", "ts-owasp-mid");
|
|
556
|
+
mid.append(el("div", "ts-owasp-name", r.name));
|
|
557
|
+
mid.append(el("div", "ts-case-why", t("test.security.needs") + ": " + r.needs));
|
|
558
|
+
mid.append(el("div", "ts-case-why", t("test.security.probe") + ": " + r.probe));
|
|
559
|
+
if (r.state === "FOUND" && r.severity) {
|
|
560
|
+
const sv = el("div", "row-actions");
|
|
561
|
+
sv.append(chip(r.severity.severity, TEST_SEV_KIND[r.severity.severity] || "bad"));
|
|
562
|
+
sv.append(el("span", "ts-case-why", r.severity.why));
|
|
563
|
+
mid.append(sv);
|
|
564
|
+
if (r.severity.cvss) mid.append(el("div", "ts-case-ev", r.severity.cvss));
|
|
565
|
+
for (const x of (r.cases || []).filter((y) => y.verdict === "fail")) {
|
|
566
|
+
mid.append(el("div", "ts-case-why", x.id + " " + x.why));
|
|
567
|
+
if (x.evidence) mid.append(el("div", "ts-case-ev", x.evidence));
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
for (const u of r.unchecked || []) mid.append(el("div", "ts-owasp-unchecked", esc(u.why)));
|
|
571
|
+
row.append(mid);
|
|
572
|
+
c.append(row);
|
|
573
|
+
}
|
|
574
|
+
for (const s of sec.statics || []) {
|
|
575
|
+
c.append(el("div", "ts-label", s.owasp + " — " + s.what));
|
|
576
|
+
for (const r of s.rows || []) c.append(el("div", "ts-route-min", (r.at || "—") + " " + r.what + " (" + r.target + ")"));
|
|
577
|
+
c.append(el("div", "note", t("test.security.statics")));
|
|
578
|
+
}
|
|
579
|
+
c.append(el("div", "note", t("test.security.uncheckedNote")));
|
|
580
|
+
c.append(el("div", "note", t("test.security.closed")));
|
|
581
|
+
c.append(el("div", "note", t("test.security.detect")));
|
|
582
|
+
return c;
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/* ---------------------------------------------------------------- 4. RUNS -- */
|
|
586
|
+
|
|
587
|
+
function testRunsTab(d) {
|
|
588
|
+
const out = frag();
|
|
589
|
+
const c = card(t("test.runs.title"));
|
|
590
|
+
// `orc test run` is where the traffic is. It is a COPY-ABLE COMMAND and never
|
|
591
|
+
// a button here.
|
|
592
|
+
c.append(laneCommand("orc test run " + d.slug, t("test.trafficWhy"), t("common.runInTerminal")));
|
|
593
|
+
out.append(c);
|
|
594
|
+
|
|
595
|
+
const runs = d.runs || [];
|
|
596
|
+
if (!runs.length) {
|
|
597
|
+
out.append(empty(t("test.runs.none"), t("test.runs.cmd")));
|
|
598
|
+
return out;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
const list = el("div", "run-list");
|
|
602
|
+
const rows = [];
|
|
603
|
+
const setOpen = (entry, open) => {
|
|
604
|
+
entry.row.classList.toggle("open", open);
|
|
605
|
+
entry.head.setAttribute("aria-expanded", String(open));
|
|
606
|
+
};
|
|
607
|
+
for (const r of runs.slice().reverse()) {
|
|
608
|
+
const row = el("div", "run-row");
|
|
609
|
+
// A row that EXPANDS IN PLACE, one at a time. `.run-card` DECLARES its
|
|
610
|
+
// column count — a card whose child count changes with its state cannot
|
|
611
|
+
// declare its rows.
|
|
612
|
+
const headBtn = el("button", "run-card has-extra");
|
|
613
|
+
headBtn.type = "button";
|
|
614
|
+
headBtn.setAttribute("aria-expanded", "false");
|
|
615
|
+
headBtn.append(el("span", "run-caret", "▸"));
|
|
616
|
+
headBtn.append(chip("run " + r.n, r.counts.fail ? "bad" : "ok"));
|
|
617
|
+
// The stop chip KEEPS ITS SLOT when there was no stop — an empty span, not
|
|
618
|
+
// an em dash. A run that ran to the end and a run whose stop was not
|
|
619
|
+
// computed are different facts, and a placeholder must assert neither.
|
|
620
|
+
headBtn.append(r.stopped ? chip(t("test.runs.stopped") + ": " + r.stopped, "warn") : el("span"));
|
|
621
|
+
const mid = el("div", "run-mid");
|
|
622
|
+
mid.append(el("div", "run-slug", r.counts.pass + " pass · " + r.counts.fail + " fail · " + r.counts.unknown + " unknown"));
|
|
623
|
+
mid.append(el("div", "run-where", r.dir));
|
|
624
|
+
headBtn.append(mid, el("div", "run-age", esc(r.at)));
|
|
625
|
+
|
|
626
|
+
const pane = el("div", "run-pane stack stack-sm");
|
|
627
|
+
if (r.stopped) pane.append(el("div", "ts-handback", t("test.runs.stoppedWhy")));
|
|
628
|
+
pane.append(kvList([[t("test.runs.dir"), r.dir]], true));
|
|
629
|
+
const mine = (d.cases || []).filter((c2) => c2.evidence && c2.evidence.indexOf("/" + r.dir + "/") !== -1);
|
|
630
|
+
for (const c2 of mine) {
|
|
631
|
+
const line = el("div", "ts-case");
|
|
632
|
+
line.append(el("span", "ts-case-id", c2.id));
|
|
633
|
+
line.append(chip(c2.verdict || t("test.cases.notRun"), TEST_VERDICT_KIND[c2.verdict] || ""));
|
|
634
|
+
const m2 = el("div", "ts-case-mid");
|
|
635
|
+
m2.append(el("div", "ts-case-target", c2.target));
|
|
636
|
+
if (c2.verdict_why) m2.append(el("div", "ts-case-why", c2.verdict_why));
|
|
637
|
+
line.append(m2, el("div", "ts-case-tail"));
|
|
638
|
+
pane.append(line);
|
|
639
|
+
}
|
|
640
|
+
const inner = el("div", "run-body-inner");
|
|
641
|
+
inner.append(pane);
|
|
642
|
+
const fold = el("div", "run-body");
|
|
643
|
+
fold.append(inner);
|
|
644
|
+
|
|
645
|
+
const entry = { row, head: headBtn, pane };
|
|
646
|
+
rows.push(entry);
|
|
647
|
+
headBtn.addEventListener("click", () => {
|
|
648
|
+
const isOpen = row.classList.contains("open");
|
|
649
|
+
for (const other of rows) if (other !== entry) setOpen(other, false);
|
|
650
|
+
setOpen(entry, !isOpen);
|
|
651
|
+
});
|
|
652
|
+
row.append(headBtn, fold);
|
|
653
|
+
list.append(row);
|
|
654
|
+
}
|
|
655
|
+
out.append(list);
|
|
656
|
+
|
|
657
|
+
// A FLAKE IS RECORDED, NEVER RETRIED AWAY.
|
|
658
|
+
if ((d.flakes || []).length) {
|
|
659
|
+
const fc = card(t("test.runs.flakes"));
|
|
660
|
+
for (const f of d.flakes)
|
|
661
|
+
fc.append(el("div", "ts-route-min", "run " + f.run + " " + f.id + " " + t("test.runs.was") + " " + f.was + " · " + t("test.runs.now") + " " + f.now));
|
|
662
|
+
fc.append(el("div", "note", t("test.runs.flakesNote")));
|
|
663
|
+
out.append(fc);
|
|
664
|
+
}
|
|
665
|
+
return out;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/* ------------------------------------------------------------ 5. FINDINGS -- */
|
|
669
|
+
|
|
670
|
+
function testFindingsTab(d, body) {
|
|
671
|
+
const out = frag();
|
|
672
|
+
|
|
673
|
+
const c = card(t("test.findings.title"));
|
|
674
|
+
const bar = el("div", "row-actions");
|
|
675
|
+
const b = el("button", "btn btn-sm", t("test.findings.report"));
|
|
676
|
+
b.type = "button";
|
|
677
|
+
b.addEventListener("click", async () => {
|
|
678
|
+
b.disabled = true;
|
|
679
|
+
setBusy(true);
|
|
680
|
+
try {
|
|
681
|
+
await post("/api/test/report", { slug: d.slug });
|
|
682
|
+
toast(t("test.findings.report"), "ok");
|
|
683
|
+
} catch (e) {
|
|
684
|
+
toast(String((e && e.message) || e), "bad");
|
|
685
|
+
} finally {
|
|
686
|
+
setBusy(false);
|
|
687
|
+
renderTest(body);
|
|
688
|
+
}
|
|
689
|
+
});
|
|
690
|
+
bar.append(b);
|
|
691
|
+
c.append(bar);
|
|
692
|
+
c.append(el("div", "note", t("test.findings.reportWhy")));
|
|
693
|
+
if (d.report_exists) c.append(kvList([[t("test.findings.reportAt"), d.paths.report]], true));
|
|
694
|
+
else c.append(el("div", "note", t("test.findings.reportMissing")));
|
|
695
|
+
c.append(el("div", "ts-handback", t("test.findings.neverStaged")));
|
|
696
|
+
out.append(c);
|
|
697
|
+
|
|
698
|
+
if (!(d.findings || []).length) {
|
|
699
|
+
out.append(empty(t("test.findings.none"), t("test.findings.evidenceNote")));
|
|
700
|
+
return out;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
for (const f of d.findings) {
|
|
704
|
+
const fc = card(f.title);
|
|
705
|
+
const row = el("div", "row-actions");
|
|
706
|
+
// OBSERVED and NOT OBSERVED are the two things a report must never blur.
|
|
707
|
+
row.append(chip(f.observed ? t("test.findings.observed") : t("test.findings.notObserved"), f.observed ? "bad" : "warn"));
|
|
708
|
+
row.append(chip(f.severity, TEST_SEV_KIND[f.severity] || ""));
|
|
709
|
+
if (f.owasp) row.append(chip(f.owasp, ""));
|
|
710
|
+
fc.append(row);
|
|
711
|
+
fc.append(el("div", "ts-case-why", f.what));
|
|
712
|
+
// ORC'S SEVERITY IS THE ONE THAT COUNTS. The model's own is kept beside it
|
|
713
|
+
// and printed only when the two disagree.
|
|
714
|
+
fc.append(el("div", "note", esc(f.severity_why)));
|
|
715
|
+
if (f.claimed_severity && String(f.claimed_severity).toLowerCase() !== String(f.severity).toLowerCase()) {
|
|
716
|
+
fc.append(el("div", "note", t("test.security.claimed") + ' "' + f.claimed_severity + '"'));
|
|
717
|
+
fc.append(el("div", "note", t("test.security.severityWho")));
|
|
718
|
+
}
|
|
719
|
+
if (!f.observed) fc.append(el("div", "note", t("test.findings.notObservedNote")));
|
|
720
|
+
const rows = [];
|
|
721
|
+
if (f.case) rows.push([t("test.findings.case"), f.case]);
|
|
722
|
+
if (f.target) rows.push([t("test.findings.target"), f.target]);
|
|
723
|
+
if (f.evidence) rows.push([t("test.cases.evidence"), f.evidence]);
|
|
724
|
+
if (f.cvss) rows.push(["CVSS", f.cvss]);
|
|
725
|
+
if (f.impact) rows.push([t("test.findings.impact"), f.impact]);
|
|
726
|
+
if (f.fix) rows.push([t("test.findings.fix"), f.fix]);
|
|
727
|
+
fc.append(kvList(rows));
|
|
728
|
+
if (f.cvss_note) fc.append(el("div", "note", esc(f.cvss_note)));
|
|
729
|
+
out.append(fc);
|
|
730
|
+
}
|
|
731
|
+
out.append(el("div", "note", t("test.findings.evidenceNote")));
|
|
732
|
+
return out;
|
|
733
|
+
}
|