penthera 1.6.0 → 1.7.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 +67 -0
- package/README.md +158 -301
- package/bin/scan.js +2 -2
- package/docs/METHODOLOGY.md +236 -182
- package/docs/commands/README.md +2 -1
- package/docs/commands/init.md +114 -0
- package/docs/commands/scan.md +1 -1
- package/docs/compare/github.md +1 -1
- package/docs/dashboard.md +95 -152
- package/docs/examples/github-actions-scan.yml +1 -1
- package/docs/internal-audit.md +1 -1
- package/docs/plan.md +3 -2
- package/docs/references.bib +43 -33
- package/docs/references.md +92 -51
- package/lib/whitebox/frameworks.js +36 -0
- package/package.json +1 -1
- package/skills/penthera/SKILL.md +19 -5
- package/src/cli/deps.js +1 -1
- package/src/cli/detect.js +125 -0
- package/src/cli/onboarding.js +428 -6
- package/src/cli/prompt.js +42 -0
- package/src/cli.js +18 -5
- package/src/ui/api/data.js +30 -0
- package/src/ui/api/demo.js +13 -0
- package/src/ui/api/routes.js +1 -0
- package/src/ui/static/app.css +286 -560
- package/src/ui/static/index.html +8 -39
- package/src/ui/static/js/app.js +105 -107
- package/src/ui/static/js/center.js +388 -0
- package/src/ui/static/js/copy.js +36 -62
- package/src/ui/static/js/dom.js +0 -38
- package/src/ui/static/js/left.js +83 -0
- package/src/ui/static/js/right.js +270 -0
- package/src/ui/static/js/state.js +35 -0
- package/src/ui/static/js/views/ai.js +0 -68
- package/src/ui/static/js/views/deps.js +0 -96
- package/src/ui/static/js/views/evidence.js +0 -115
- package/src/ui/static/js/views/findings.js +0 -165
- package/src/ui/static/js/views/history.js +0 -92
- package/src/ui/static/js/views/overview.js +0 -184
- package/src/ui/static/js/views/run.js +0 -349
- package/src/ui/static/js/views/settings.js +0 -67
package/src/ui/static/index.html
CHANGED
|
@@ -12,45 +12,14 @@
|
|
|
12
12
|
<script type="module" src="/js/app.js"></script>
|
|
13
13
|
</head>
|
|
14
14
|
<body>
|
|
15
|
-
<a class="skip" href="#
|
|
16
|
-
<
|
|
17
|
-
<
|
|
18
|
-
|
|
19
|
-
penthera
|
|
20
|
-
</
|
|
21
|
-
<
|
|
22
|
-
|
|
23
|
-
<li><a href="#/overview" data-view="overview"><span class="key">1</span>Overview</a></li>
|
|
24
|
-
<li><a href="#/findings" data-view="findings"><span class="key">2</span>Findings</a></li>
|
|
25
|
-
<li><a href="#/run" data-view="run"><span class="key">3</span>Run</a></li>
|
|
26
|
-
<li><a href="#/history" data-view="history"><span class="key">4</span>History</a></li>
|
|
27
|
-
<li><a href="#/deps" data-view="deps"><span class="key">5</span>Dependencies</a></li>
|
|
28
|
-
<li><a href="#/ai" data-view="ai"><span class="key">6</span>AI tooling</a></li>
|
|
29
|
-
<li><a href="#/evidence" data-view="evidence"><span class="key">7</span>Evidence</a></li>
|
|
30
|
-
<li><a href="#/settings" data-view="settings"><span class="key">8</span>Settings</a></li>
|
|
31
|
-
</ol>
|
|
32
|
-
</nav>
|
|
33
|
-
<div class="mb-right">
|
|
34
|
-
<span id="mb-chips"></span>
|
|
35
|
-
<button type="button" class="themebtn" id="theme-btn" aria-label="Switch theme">◐ dark</button>
|
|
36
|
-
<span class="mb-clock" id="mb-clock" aria-hidden="true"></span>
|
|
37
|
-
</div>
|
|
38
|
-
</header>
|
|
39
|
-
<div class="desktop">
|
|
40
|
-
<div class="window">
|
|
41
|
-
<div class="titlebar"><span class="box close" aria-hidden="true"></span><span class="title" id="win-title">penthera :: overview</span><span class="box zoom" aria-hidden="true"></span></div>
|
|
42
|
-
<div class="prompt" id="prompt"><span class="user">penthera@127.0.0.1</span>:<span class="path">~/overview</span>$ <span id="prompt-cmd"></span><span class="cursor" aria-hidden="true"></span></div>
|
|
43
|
-
<main class="main" id="main" tabindex="-1">
|
|
44
|
-
<noscript><p>penthera ui needs JavaScript: every screen reads from the local API on this machine.</p></noscript>
|
|
45
|
-
</main>
|
|
46
|
-
<footer class="status">
|
|
47
|
-
<span><kbd>1</kbd>-<kbd>8</kbd> screens</span>
|
|
48
|
-
<span><kbd>/</kbd> filter findings</span>
|
|
49
|
-
<span><kbd>j</kbd> <kbd>k</kbd> move in tables</span>
|
|
50
|
-
<span><kbd>y</kbd> <kbd>n</kbd> <kbd>q</kbd> approve, skip, stop a diff</span>
|
|
51
|
-
<span id="status-bind"></span>
|
|
52
|
-
</footer>
|
|
53
|
-
</div>
|
|
15
|
+
<a class="skip" href="#center">Skip to content</a>
|
|
16
|
+
<div class="app" id="app">
|
|
17
|
+
<aside class="left" id="left" aria-label="Projects and runs"></aside>
|
|
18
|
+
<main class="center" id="center" tabindex="-1">
|
|
19
|
+
<noscript><p class="pad">penthera ui needs JavaScript: every view reads from the local API on this machine.</p></noscript>
|
|
20
|
+
</main>
|
|
21
|
+
<aside class="right" id="right" aria-label="Details"></aside>
|
|
22
|
+
<div class="scrim" id="scrim" hidden></div>
|
|
54
23
|
</div>
|
|
55
24
|
<div class="sr" role="status" aria-live="polite" id="announce"></div>
|
|
56
25
|
</body>
|
package/src/ui/static/js/app.js
CHANGED
|
@@ -1,138 +1,136 @@
|
|
|
1
|
-
// penthera ui:
|
|
1
|
+
// penthera ui: one view. Left: projects and runs. Centre: the selected project
|
|
2
|
+
// or run. Right: cards, or the open finding, or settings.
|
|
2
3
|
import { h, clear, announce } from "./dom.js";
|
|
3
4
|
import { api } from "./api.js";
|
|
4
|
-
import { typeInto } from "./typer.js";
|
|
5
5
|
import { patternDefs } from "./charts.js";
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
8
|
-
import
|
|
9
|
-
import
|
|
10
|
-
import
|
|
11
|
-
import * as deps from "./views/deps.js";
|
|
12
|
-
import * as ai from "./views/ai.js";
|
|
13
|
-
import * as evidence from "./views/evidence.js";
|
|
14
|
-
import * as settings from "./views/settings.js";
|
|
6
|
+
import { state, set, on } from "./state.js";
|
|
7
|
+
import { renderLeft } from "./left.js";
|
|
8
|
+
import { renderCenter, renderBar, refreshList, moveFinding, answerPending } from "./center.js";
|
|
9
|
+
import { renderRight } from "./right.js";
|
|
10
|
+
import { ERR } from "./copy.js";
|
|
15
11
|
|
|
16
|
-
const VIEWS = { overview, findings, run, history, deps, ai, evidence, settings };
|
|
17
|
-
const ORDER = Object.keys(VIEWS);
|
|
18
12
|
const THEME_KEY = "penthera-ui-theme";
|
|
13
|
+
const PANELS_KEY = "penthera-ui-panels";
|
|
14
|
+
const app = document.getElementById("app");
|
|
15
|
+
const left = document.getElementById("left");
|
|
16
|
+
const center = document.getElementById("center");
|
|
17
|
+
const right = document.getElementById("right");
|
|
18
|
+
const scrim = document.getElementById("scrim");
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
// ── Theme: one system, [data-theme] on <html>; theme.js set it before paint ──
|
|
21
|
+
const theme = {
|
|
22
|
+
get: () => (document.documentElement.getAttribute("data-theme") === "light" ? "light" : "dark"),
|
|
23
|
+
set(t) {
|
|
24
|
+
document.documentElement.setAttribute("data-theme", t);
|
|
25
|
+
try { localStorage.setItem(THEME_KEY, t); } catch { /* storage off: this page only */ }
|
|
26
|
+
announce(`${t === "light" ? "Light" : "Dark"} theme`);
|
|
27
|
+
},
|
|
28
|
+
};
|
|
24
29
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
+
// ── Side panels: [ and ] toggle; drawers below 1180px / 760px ──
|
|
31
|
+
const narrowRight = () => window.matchMedia("(max-width: 1180px)").matches;
|
|
32
|
+
const narrowLeft = () => window.matchMedia("(max-width: 760px)").matches;
|
|
33
|
+
function savedPanels() {
|
|
34
|
+
try { return JSON.parse(localStorage.getItem(PANELS_KEY) || "null"); } catch { return null; }
|
|
30
35
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
if (
|
|
36
|
-
|
|
36
|
+
function applyPanels({ left: l, right: r }) {
|
|
37
|
+
app.classList.toggle("no-left", !l);
|
|
38
|
+
app.classList.toggle("no-right", !r);
|
|
39
|
+
scrim.hidden = !((r && narrowRight()) || (l && narrowLeft()));
|
|
40
|
+
if (!narrowRight()) {
|
|
41
|
+
try { localStorage.setItem(PANELS_KEY, JSON.stringify({ left: l, right: r })); } catch { /* per-viewer convenience only */ }
|
|
42
|
+
}
|
|
37
43
|
}
|
|
44
|
+
const panels = { left: true, right: true, ...(savedPanels() || {}) };
|
|
45
|
+
if (narrowLeft()) panels.left = false;
|
|
46
|
+
if (narrowRight()) panels.right = false;
|
|
47
|
+
applyPanels(panels);
|
|
48
|
+
function toggleLeft() { panels.left = !panels.left; applyPanels(panels); }
|
|
49
|
+
function toggleRight() { panels.right = !panels.right; applyPanels(panels); }
|
|
50
|
+
scrim.addEventListener("click", () => {
|
|
51
|
+
if (narrowLeft()) panels.left = false;
|
|
52
|
+
if (narrowRight()) panels.right = false;
|
|
53
|
+
applyPanels(panels);
|
|
54
|
+
});
|
|
38
55
|
|
|
39
|
-
// ──
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
|
|
56
|
+
// ── Context the renderers share ──
|
|
57
|
+
const ctx = {
|
|
58
|
+
theme,
|
|
59
|
+
toggleLeft,
|
|
60
|
+
toggleRight,
|
|
61
|
+
refresh,
|
|
62
|
+
refreshMeta: async () => { state.meta = await api.get("/api/meta"); },
|
|
63
|
+
redrawRight: () => renderRight(right, ctx),
|
|
64
|
+
openFinding(f) {
|
|
65
|
+
set({ finding: { id: f.id, project: f.project }, right: "finding" });
|
|
66
|
+
if (!panels.right) { panels.right = true; applyPanels(panels); }
|
|
67
|
+
for (const b of document.querySelectorAll("#findings-list .row")) {
|
|
68
|
+
b.setAttribute("aria-current", String(b.dataset.id === f.id && b.dataset.project === f.project));
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
focusBar(kind) {
|
|
72
|
+
if (kind && kind !== state.mode) set({ mode: kind });
|
|
73
|
+
if (narrowLeft() && panels.left) { panels.left = false; applyPanels(panels); }
|
|
74
|
+
const input = document.getElementById("bar-input");
|
|
75
|
+
(input && !input.disabled ? input : document.querySelector(".bar .btn.primary"))?.focus();
|
|
76
|
+
},
|
|
77
|
+
};
|
|
55
78
|
|
|
56
|
-
function
|
|
57
|
-
const
|
|
58
|
-
|
|
79
|
+
async function refresh() {
|
|
80
|
+
const [overview, findings, runs] = await Promise.all([api.get("/api/overview"), api.get("/api/findings"), api.get("/api/runs")]);
|
|
81
|
+
set({ overview, findings: findings.findings, runs });
|
|
59
82
|
}
|
|
60
|
-
clock();
|
|
61
|
-
setInterval(clock, 15_000);
|
|
62
83
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
84
|
+
// ── Redraw on state changes, one column at a time ──
|
|
85
|
+
on((patch) => {
|
|
86
|
+
const keys = Object.keys(patch);
|
|
87
|
+
if (keys.includes("query") && keys.length === 1) { refreshList(ctx); return; }
|
|
88
|
+
if (keys.some((k) => ["overview", "runs", "sel", "meta", "right"].includes(k))) renderLeft(left, { onNewScan: () => ctx.focusBar("scan"), toggleLeft, theme });
|
|
89
|
+
if (keys.some((k) => ["overview", "findings", "sel", "mode"].includes(k))) {
|
|
90
|
+
// A live transcript is rebuilt only when the selection changes; data refreshes redraw the bar.
|
|
91
|
+
if (state.sel.kind === "run" && !keys.includes("sel")) renderBar(center, ctx);
|
|
92
|
+
else renderCenter(center, ctx);
|
|
93
|
+
if (keys.includes("sel") && narrowLeft() && panels.left) { panels.left = false; applyPanels(panels); }
|
|
70
94
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
document.querySelector("#prompt .path").textContent = `~/${name}`;
|
|
75
|
-
typeInto(document.getElementById("prompt-cmd"), `penthera ui --view ${name}`);
|
|
76
|
-
|
|
77
|
-
if (cleanup) { try { cleanup(); } catch { /* view already gone */ } cleanup = null; }
|
|
78
|
-
clear(main);
|
|
79
|
-
const root = h("div"); // no enter animation: screens switch from the keyboard (1-8)
|
|
80
|
-
main.append(root);
|
|
81
|
-
try {
|
|
82
|
-
const c = await VIEWS[name].render(root, ctx);
|
|
83
|
-
if (seq !== renderSeq) { c?.(); return; }
|
|
84
|
-
cleanup = c || null;
|
|
85
|
-
} catch (e) {
|
|
86
|
-
if (seq !== renderSeq) return;
|
|
87
|
-
clear(root).append(
|
|
88
|
-
h("h1", {}, title),
|
|
89
|
-
h("p", { class: "error", role: "alert" }, e.message),
|
|
90
|
-
e.status === 401 ? h("p", { class: "muted" }, ERR.session) : null,
|
|
91
|
-
);
|
|
95
|
+
if (keys.some((k) => ["sel", "right", "finding"].includes(k)) || (keys.includes("runs") && state.right === "cards")) {
|
|
96
|
+
if (patch.openRight && !panels.right) { panels.right = true; applyPanels(panels); }
|
|
97
|
+
renderRight(right, ctx);
|
|
92
98
|
}
|
|
93
|
-
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function chrome(meta) {
|
|
97
|
-
// Only real elements go into the bar: a null child would render as the text "null".
|
|
98
|
-
const chips = [
|
|
99
|
-
meta.demo ? h("span", { class: "chip inv", title: "Fake data seeded by --demo" }, "DEMO DATA") : null,
|
|
100
|
-
h("span", { class: meta.settings.offline ? "chip on" : "chip" }, meta.settings.offline ? "offline" : "online"),
|
|
101
|
-
].filter(Boolean);
|
|
102
|
-
clear(document.getElementById("mb-chips")).append(...chips);
|
|
103
|
-
document.getElementById("status-bind").textContent = `v${meta.version} :: bound to ${location.host} :: no network :: no telemetry`;
|
|
104
|
-
}
|
|
99
|
+
});
|
|
105
100
|
|
|
101
|
+
// ── Keyboard ──
|
|
106
102
|
function isTyping(e) {
|
|
107
103
|
const t = e.target;
|
|
108
104
|
return t instanceof HTMLElement && (t.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName));
|
|
109
105
|
}
|
|
110
|
-
|
|
111
106
|
document.addEventListener("keydown", (e) => {
|
|
112
|
-
if (e.metaKey || e.ctrlKey || e.altKey
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
107
|
+
if (e.metaKey || e.ctrlKey || e.altKey) return;
|
|
108
|
+
if (e.key === "Escape") {
|
|
109
|
+
const q = document.getElementById("q");
|
|
110
|
+
if (document.activeElement === q && q.value) { q.value = ""; set({ query: "" }); return; }
|
|
111
|
+
if (state.right !== "cards") { set({ right: "cards", finding: null }); return; }
|
|
112
|
+
if ((narrowRight() && panels.right) || (narrowLeft() && panels.left)) scrim.click();
|
|
113
|
+
return;
|
|
118
114
|
}
|
|
115
|
+
if (isTyping(e)) return;
|
|
116
|
+
if (answerPending(e.key)) { e.preventDefault(); return; }
|
|
117
|
+
if (e.key === "/") { const q = document.getElementById("q"); if (q) { e.preventDefault(); q.focus(); } return; }
|
|
118
|
+
if (e.key === "j" || e.key === "k") { e.preventDefault(); moveFinding(e.key === "j" ? 1 : -1, ctx); return; }
|
|
119
|
+
if (e.key === "[") { e.preventDefault(); toggleLeft(); return; }
|
|
120
|
+
if (e.key === "]") { e.preventDefault(); toggleRight(); }
|
|
119
121
|
});
|
|
120
122
|
|
|
121
|
-
window.addEventListener("hashchange", render);
|
|
122
|
-
|
|
123
123
|
(async function boot() {
|
|
124
124
|
document.body.append(patternDefs());
|
|
125
125
|
try {
|
|
126
|
-
|
|
127
|
-
|
|
126
|
+
state.meta = await api.get("/api/meta");
|
|
127
|
+
await refresh();
|
|
128
|
+
document.title = `penthera ui${state.meta.demo ? " :: demo" : ""}`;
|
|
128
129
|
} catch (e) {
|
|
129
|
-
clear(
|
|
130
|
+
clear(center).append(h("div", { class: "col" },
|
|
130
131
|
h("h1", {}, "Not signed in"),
|
|
131
|
-
h("p", { class: "
|
|
132
|
-
h("p", { class: "muted" }, "Run penthera ui and open the one-time link it prints. The link works once."),
|
|
133
|
-
);
|
|
134
|
-
return;
|
|
132
|
+
h("p", { class: "bad", role: "alert" }, e.message),
|
|
133
|
+
h("p", { class: "muted" }, e.status === 401 ? ERR.session : "Run penthera ui and open the one-time link it prints. The link works once."),
|
|
134
|
+
));
|
|
135
135
|
}
|
|
136
|
-
ctx.refreshMeta = async () => { ctx.meta = await api.get("/api/meta"); chrome(ctx.meta); };
|
|
137
|
-
render();
|
|
138
136
|
})();
|