penthera 1.7.0 → 1.8.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 +315 -0
- package/README.md +139 -76
- package/SECURITY.md +8 -6
- package/docs/examples/pr-dependency-review.yml +1 -1
- package/lib/adaptive-probes.js +3 -3
- package/lib/advisories/evidence.js +248 -0
- package/lib/advisories/index.js +228 -0
- package/lib/advisories/products.js +83 -0
- package/lib/advisories/versions.js +65 -0
- package/lib/agents/hidden-content.js +122 -11
- package/lib/agents/index.js +26 -6
- package/lib/agents/lock.js +33 -5
- package/lib/agents/rules.js +33 -4
- package/lib/compliance/index.js +4 -4
- package/lib/crawler.js +12 -15
- package/lib/deps/capabilities.js +21 -5
- package/lib/deps/findings.js +309 -0
- package/lib/deps/index.js +13 -272
- package/lib/deps/iocs.js +74 -0
- package/lib/deps/lockfile.js +3 -8
- package/lib/deps/online.js +8 -3
- package/lib/deps/popular-packages.json.gz +0 -0
- package/lib/deps/popular-pypi.json.gz +0 -0
- package/lib/deps/python/capabilities.js +16 -3
- package/lib/deps/python/index.js +5 -1
- package/lib/deps/python/lockfiles.js +11 -5
- package/lib/deps/python/source.js +75 -26
- package/lib/deps/python/toml.js +15 -3
- package/lib/deps/source.js +28 -9
- package/lib/deps/typosquat.js +30 -5
- package/lib/fix/apply.js +253 -34
- package/lib/fix/index.js +7 -4
- package/lib/fix/model.js +25 -5
- package/lib/fix/util.js +6 -2
- package/lib/fuzzer.js +3 -3
- package/lib/git.js +65 -0
- package/lib/hook/index.js +157 -73
- package/lib/hook/policy.js +10 -4
- package/lib/install/index.js +4 -7
- package/lib/machine.js +0 -13
- package/lib/memory/store.js +11 -4
- package/lib/nuclei-loader.js +0 -25
- package/lib/owasp-wstg.js +4 -0
- package/lib/plugins.js +1 -1
- package/lib/provenance.js +1 -0
- package/lib/recon.js +12 -31
- package/lib/references.json +224 -0
- package/lib/retirejs.js +4 -4
- package/lib/rules/advisories/advisories.json +186 -0
- package/lib/rules/advisories/eol.json +413 -0
- package/lib/rules/advisories/pack.json +20 -0
- package/lib/rules/advisories/pack.json.sig +6 -0
- package/lib/rules/allowed_signers +13 -0
- package/lib/rules/core/agent-exec.json +29 -0
- package/lib/rules/core/agent-instructions.json +17 -0
- package/lib/rules/core/pack.json +30 -0
- package/lib/rules/core/pack.json.sig +6 -0
- package/lib/rules/core/secrets.json +161 -0
- package/lib/rules/core/templates.json +52 -0
- package/lib/rules/index.js +107 -0
- package/lib/rules/ioc-2025-2026/files.json +43 -0
- package/lib/rules/ioc-2025-2026/hosts.json +35 -0
- package/lib/rules/ioc-2025-2026/pack.json +25 -0
- package/lib/rules/ioc-2025-2026/pack.json.sig +6 -0
- package/lib/rules/ioc-2025-2026/packages.json +1048 -0
- package/lib/rules/pack.js +452 -0
- package/lib/rules/regex-guard.js +81 -9
- package/lib/rules/sshsig.js +205 -0
- package/lib/templates.js +24 -20
- package/lib/tmp.js +21 -0
- package/lib/whitebox/frameworks.js +1 -1
- package/lib/whitebox/secrets.js +117 -41
- package/package.json +12 -4
- package/scripts/rules/build-pack.mjs +80 -0
- package/scripts/rules/check-bundled.mjs +30 -0
- package/scripts/rules/sign-pack.sh +32 -0
- package/skills/penthera/SKILL.md +16 -15
- package/src/agent/git.js +147 -14
- package/src/agent/loop.js +165 -46
- package/src/cli/agent.js +11 -5
- package/src/cli/deps.js +0 -2
- package/src/cli/detect.js +14 -7
- package/src/cli/fix.js +26 -8
- package/src/cli/hook.js +2 -1
- package/src/cli/onboarding.js +14 -6
- package/src/cli/report-outputs.js +1 -0
- package/src/cli/rules.js +144 -0
- package/src/cli/ui.js +43 -10
- package/src/cli.js +24 -1
- package/src/report/evidence.js +105 -21
- package/src/report/footer.js +1 -1
- package/src/report/md-escape.js +8 -0
- package/src/scan-repo.js +6 -0
- package/src/scan-url.js +11 -3
- package/src/ui/api/data.js +39 -6
- package/src/ui/api/routes.js +44 -11
- package/src/ui/api/runs.js +32 -7
- package/src/ui/security.js +20 -23
- package/src/ui/server.js +52 -27
- package/src/ui/static/app.css +583 -213
- package/src/ui/static/favicon.svg +1 -1
- package/src/ui/static/fonts/DepartureMono-LICENSE.txt +93 -0
- package/src/ui/static/fonts/DepartureMono-Regular.woff2 +0 -0
- package/src/ui/static/index.html +5 -4
- package/src/ui/static/js/api.js +114 -6
- package/src/ui/static/js/app.js +81 -42
- package/src/ui/static/js/center.js +246 -133
- package/src/ui/static/js/charts.js +99 -18
- package/src/ui/static/js/copy.js +68 -9
- package/src/ui/static/js/dialog.js +152 -0
- package/src/ui/static/js/left.js +23 -60
- package/src/ui/static/js/menubar.js +170 -0
- package/src/ui/static/js/onboarding.js +104 -0
- package/src/ui/static/js/panels.js +186 -0
- package/src/ui/static/js/right.js +69 -188
- package/src/ui/static/js/state.js +36 -5
- package/src/ui/static/logo.svg +10 -0
- package/src/ui/static/mark.svg +3 -0
- package/src/utils/http.js +162 -13
- package/docs/METHODOLOGY.md +0 -375
- package/docs/commands/README.md +0 -24
- package/docs/commands/action.md +0 -21
- package/docs/commands/agents.md +0 -38
- package/docs/commands/deps.md +0 -115
- package/docs/commands/fix.md +0 -53
- package/docs/commands/hook.md +0 -69
- package/docs/commands/init.md +0 -114
- package/docs/commands/install.md +0 -53
- package/docs/commands/scan.md +0 -100
- package/docs/commands/skill.md +0 -58
- package/docs/compare/README.md +0 -25
- package/docs/compare/github.md +0 -60
- package/docs/compare/semgrep.md +0 -54
- package/docs/compare/snyk.md +0 -62
- package/docs/compare/socket.md +0 -78
- package/docs/compare/zap.md +0 -59
- package/docs/compliance-cra.md +0 -141
- package/docs/compliance-nis2.md +0 -168
- package/docs/dashboard.md +0 -187
- package/docs/evidence.md +0 -162
- package/docs/funding/due-diligence-checklist.md +0 -79
- package/docs/funding/nlnet-ngi-zero-draft.md +0 -273
- package/docs/funding/partner-one-pager.md +0 -84
- package/docs/funding/sponsor-tiers.md +0 -81
- package/docs/index.md +0 -81
- package/docs/internal-audit.md +0 -83
- package/docs/local-loop.md +0 -276
- package/docs/marketing.md +0 -151
- package/docs/owasp-wstg-coverage.md +0 -79
- package/docs/plan.md +0 -102
- package/docs/privacy.md +0 -38
- package/docs/references.bib +0 -559
- package/docs/references.md +0 -119
- package/docs/release-plan.md +0 -228
- package/docs/research/socket-positioning-notes.md +0 -183
- package/docs/use-cases/README.md +0 -14
- package/docs/use-cases/agencies.md +0 -71
- package/docs/use-cases/ai-coding-teams.md +0 -81
- package/docs/use-cases/eu-sme-compliance.md +0 -68
- package/docs/use-cases/open-source-maintainers.md +0 -75
- package/docs/use-cases/solo-founder.md +0 -65
- package/lib/deps/popular-packages.json +0 -1
- package/lib/deps/popular-pypi.json +0 -1
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
// The
|
|
2
|
-
//
|
|
3
|
-
import { h, clear, empty, sevBadge, diffBlock
|
|
1
|
+
// The inspector: one finding in plain words with its evidence and fix, or
|
|
2
|
+
// settings, or a short note on what goes here. Nothing else lives on the right.
|
|
3
|
+
import { h, clear, empty, sevBadge, diffBlock } from "./dom.js";
|
|
4
4
|
import { api } from "./api.js";
|
|
5
|
-
import { state, set } from "./state.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
8
|
-
import { EMPTY, KEYS } from "./copy.js";
|
|
5
|
+
import { state, set, simple, countsInView, currentProject } from "./state.js";
|
|
6
|
+
import { halftone, riskLevel } from "./charts.js";
|
|
7
|
+
import { SEVERITIES } from "./format.js";
|
|
8
|
+
import { EMPTY, KEYS, SEV_WORD } from "./copy.js";
|
|
9
9
|
|
|
10
10
|
let seq = 0;
|
|
11
11
|
|
|
@@ -13,188 +13,33 @@ export async function renderRight(el, ctx) {
|
|
|
13
13
|
const my = ++seq;
|
|
14
14
|
if (state.right === "finding" && state.finding) return findingView(el, ctx, my);
|
|
15
15
|
if (state.right === "settings") return settingsView(el, ctx, my);
|
|
16
|
-
return
|
|
16
|
+
return emptyView(el, ctx);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
function
|
|
20
|
-
return h("
|
|
21
|
-
h("
|
|
22
|
-
h("
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function head(title, ctx, extra) {
|
|
26
|
-
return h("div", { class: "detail-head" },
|
|
27
|
-
h("span", { class: "t1" }, title),
|
|
19
|
+
function head(title, ctx, extra, closable = true) {
|
|
20
|
+
return h("div", { class: "win-bar" },
|
|
21
|
+
closable ? h("button", { type: "button", class: "icon close-box", "aria-label": "Close (Esc)", title: "Close Esc", onclick: () => set({ right: "none", finding: null }) }) : null,
|
|
22
|
+
h("span", { class: "title" }, title),
|
|
28
23
|
h("span", { class: "grow" }),
|
|
29
24
|
extra || null,
|
|
30
|
-
h("button", { type: "button", class: "icon", "aria-label": "
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// ── Cards ───────────────────────────────────────────────────────────────
|
|
34
|
-
|
|
35
|
-
async function cardsView(el, ctx, my) {
|
|
36
|
-
const project = state.sel.kind === "project" ? state.sel.key : state.sel.kind === "past" ? state.sel.past?.target?.repo : state.sel.run?.repo || null;
|
|
37
|
-
const [env, mem, deps, ai, ev, settings] = await Promise.all([
|
|
38
|
-
api.get(`/api/env?project=${encodeURIComponent(project || "")}`),
|
|
39
|
-
api.get("/api/memory"), api.get("/api/deps"), api.get("/api/ai"), api.get("/api/evidence"), api.get("/api/settings"),
|
|
40
|
-
]).catch((e) => { clear(el).append(h("p", { class: "pad error" }, e.message)); return []; });
|
|
41
|
-
if (my !== seq || !env) return;
|
|
42
|
-
|
|
43
|
-
clear(el).append(h("div", { class: "cards" },
|
|
44
|
-
envCard(env, settings, project, ctx),
|
|
45
|
-
memoryCard(mem.projects),
|
|
46
|
-
depsCard(deps),
|
|
47
|
-
aiCard(ai),
|
|
48
|
-
evidenceCard(ev.bundles, project, ctx),
|
|
49
|
-
changesCard(env, project),
|
|
50
|
-
));
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
function envCard(env, settings, project, ctx) {
|
|
54
|
-
const sw = h("button", { type: "button", class: "switch", role: "switch", "aria-checked": String(settings.offline) }, h("span", {}, "Offline"), h("span", { class: "state" }, settings.offline ? "on" : "off"));
|
|
55
|
-
sw.addEventListener("click", async () => {
|
|
56
|
-
const s = await api.post("/api/settings", { offline: sw.getAttribute("aria-checked") !== "true" });
|
|
57
|
-
sw.setAttribute("aria-checked", String(s.offline));
|
|
58
|
-
sw.lastChild.textContent = s.offline ? "on" : "off";
|
|
59
|
-
announce(`Offline ${s.offline ? "on" : "off"}`);
|
|
60
|
-
ctx.refreshMeta?.();
|
|
61
|
-
});
|
|
62
|
-
return card("Environment", settings.offline ? "offline" : "online", true,
|
|
63
|
-
h("dl", { class: "kv" },
|
|
64
|
-
h("dt", {}, "folder"), h("dd", {}, env.folder || (project ? "a URL target" : state.meta?.projects?.[0] || "-")),
|
|
65
|
-
h("dt", {}, "branch"), h("dd", {}, env.branch ? `${env.branch}${env.head ? ` @ ${env.head}` : ""}` : "-"),
|
|
66
|
-
h("dt", {}, "bound to"), h("dd", {}, `${location.host}, no network`),
|
|
67
|
-
),
|
|
68
|
-
sw,
|
|
69
|
-
h("p", { class: "muted" }, settings.offline ? "Scans started here contact only their target." : "Scans may also contact recon sources and fetch the Retire.js database."),
|
|
70
|
-
env.hosts.length ? h("div", {},
|
|
71
|
-
h("p", { class: "label" }, "Hosts the last scan contacted"),
|
|
72
|
-
h("ul", { class: "mini" }, env.hosts.map((x) => h("li", {}, h("span", { class: x.thirdParty ? "t1" : "ok" }, x.thirdParty ? "3P" : "OK"), h("span", { class: "grow" }, x.host), h("span", { class: "muted" }, String(x.requests))))),
|
|
73
|
-
env.blocked.length ? h("ul", { class: "mini" }, env.blocked.map((x) => h("li", {}, h("span", { class: "muted" }, "--"), h("span", { class: "grow muted" }, `${x.host} (blocked)`)))) : null,
|
|
74
|
-
) : null,
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function memoryCard(projects) {
|
|
79
|
-
if (!projects.length) return card("Memory", "", false, empty(EMPTY.memory));
|
|
80
|
-
const recipes = new Map();
|
|
81
|
-
for (const p of projects) for (const r of p.recipes) {
|
|
82
|
-
const c = recipes.get(r.recipe) || { verified: 0, failed: 0 };
|
|
83
|
-
c.verified += r.verified;
|
|
84
|
-
c.failed += r.failed;
|
|
85
|
-
recipes.set(r.recipe, c);
|
|
86
|
-
}
|
|
87
|
-
const broken = projects.filter((p) => p.chain !== "ok");
|
|
88
|
-
const ttf = projects.map((p) => p.medianTimeToFixDays).filter((x) => x != null);
|
|
89
|
-
return card("Memory", broken.length ? `${broken.length} broken` : "chain ok", false,
|
|
90
|
-
h("dl", { class: "kv" },
|
|
91
|
-
h("dt", {}, "chain"), h("dd", { class: broken.length ? "bad" : "ok" }, broken.length ? `[!] broken in ${plural(broken.length, "project")} :: ignored there` : "[OK] sha-256 hash chain"),
|
|
92
|
-
h("dt", {}, "events"), h("dd", {}, String(projects.reduce((n, p) => n + p.events, 0))),
|
|
93
|
-
h("dt", {}, "fixes"), h("dd", {}, `${projects.reduce((n, p) => n + p.verifiedFixes, 0)} verified · ${projects.reduce((n, p) => n + p.failedFixes, 0)} failed`),
|
|
94
|
-
h("dt", {}, "time to fix"), h("dd", {}, ttf.length ? `median ${Math.min(...ttf)}-${Math.max(...ttf)} days` : "-"),
|
|
95
|
-
),
|
|
96
|
-
h("p", { class: "label" }, "Recipe success"),
|
|
97
|
-
bars([...recipes].map(([recipe, r]) => ({ label: recipe, value: r.verified })), { width: 10 }),
|
|
98
|
-
h("p", { class: "muted" }, [...recipes].map(([recipe, r]) => `${recipe} ${pct(r.verified / Math.max(1, r.verified + r.failed))}`).join(" · ")),
|
|
99
|
-
);
|
|
25
|
+
h("button", { type: "button", class: "icon", "aria-label": "Hide the inspector (])", title: "Hide the inspector ]", onclick: ctx.toggleRight }, "⇥"));
|
|
100
26
|
}
|
|
101
27
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
function depsCard(d) {
|
|
105
|
-
const l = d.latest;
|
|
106
|
-
if (!l) return card("Dependencies", "", false, empty(EMPTY.deps));
|
|
107
|
-
const diff = l.diff;
|
|
108
|
-
const gained = diff ? diff.upgraded.reduce((n, u) => n + u.gained.length, 0) + diff.added.reduce((n, a) => n + a.capabilities.length, 0) : 0;
|
|
109
|
-
const chip = (id, plus) => h("span", { class: `chip ${CAP_SEV[id] ? "inv" : ""}` }, `${plus ? "+" : ""}${id}`);
|
|
110
|
-
return card("Dependencies", `${l.ecosystem} · ${l.total} packages`, false,
|
|
111
|
-
h("dl", { class: "kv" },
|
|
112
|
-
h("dt", {}, "packages"), h("dd", {}, `${l.total} (${l.direct} direct) · ${l.lockfile}`),
|
|
113
|
-
h("dt", {}, "diff"), h("dd", {}, diff ? `+${diff.added.length} added · ~${diff.upgraded.length} upgraded · -${diff.removed.length} removed vs ${diff.base}` : "no base compared"),
|
|
114
|
-
h("dt", {}, "gained"), h("dd", {}, `${gained} capabilities`),
|
|
115
|
-
h("dt", {}, "typosquats"), h("dd", { class: l.typosquats.length ? "bad" : null }, l.typosquats.length ? l.typosquats.map((t) => `${t.name} ~ ${t.similarTo}`).join(", ") : "none"),
|
|
116
|
-
),
|
|
117
|
-
diff ? h("ul", { class: "mini" },
|
|
118
|
-
diff.added.map((a) => h("li", { class: "stacked" }, h("span", {}, `+ ${a.name}@${a.version}`), h("span", { class: "chips" }, a.capabilities.slice(0, 4).map((c) => chip(c, true))))),
|
|
119
|
-
diff.upgraded.filter((u) => u.gained.length).map((u) => h("li", { class: "stacked" }, h("span", {}, `${u.name} ${u.from} → ${u.to}`), h("span", { class: "chips" }, u.gained.slice(0, 4).map((c) => chip(c, true)))))) : null,
|
|
120
|
-
h("p", { class: "label" }, "Capabilities in the tree"),
|
|
121
|
-
bars(Object.entries(l.capabilityCounts).filter(([, n]) => n > 0).sort((a, b) => b[1] - a[1]).slice(0, 6).map(([id, n]) => ({ label: id, value: n, ch: SHADE[CAP_SEV[id] || "low"] })), { width: 10 }),
|
|
122
|
-
d.installs.length ? h("div", {},
|
|
123
|
-
h("p", { class: "label" }, "Install firewall"),
|
|
124
|
-
h("ul", { class: "mini" }, d.installs.slice(0, 5).map((i) => {
|
|
125
|
-
const deny = i.changes.some((c) => c.deny.length) || i.action === "refused";
|
|
126
|
-
return h("li", {}, h("span", { class: deny ? "bad" : i.clean ? "ok" : "t1" }, deny ? "REFUSED" : i.clean ? "CLEAN" : "APPROVED"), h("span", { class: "grow" }, i.specs.join(" ")), h("span", { class: "muted" }, ago(i.ts)));
|
|
127
|
-
}))) : null,
|
|
128
|
-
);
|
|
129
|
-
}
|
|
28
|
+
// ── Nothing selected ────────────────────────────────────────────────────
|
|
130
29
|
|
|
131
|
-
function
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
h("div", { class: "
|
|
138
|
-
|
|
139
|
-
h("
|
|
140
|
-
|
|
141
|
-
|
|
30
|
+
function emptyView(el, ctx) {
|
|
31
|
+
const counts = countsInView();
|
|
32
|
+
const risk = riskLevel(counts);
|
|
33
|
+
const said = counts ? SEVERITIES.filter((s) => counts[s]).map((s) => `${counts[s]} ${s}`).join(", ") : "";
|
|
34
|
+
clear(el).append(
|
|
35
|
+
head("Inspector", ctx, null, false),
|
|
36
|
+
h("div", { class: "pane-body detail inspector-empty" },
|
|
37
|
+
h("p", { class: "prose" }, EMPTY.inspector),
|
|
38
|
+
simple() ? null : h("section", {}, h("p", { class: "label" }, "Keys"), h("dl", { class: "kv" }, KEYS.flatMap(([k, v]) => [h("dt", {}, h("kbd", {}, k)), h("dd", {}, v)]))),
|
|
39
|
+
// The field: dark with dots when something serious is open, plain paper when nothing is.
|
|
40
|
+
halftone({ risk, seed: currentProject() || "all projects", label: risk ? `Open risk: ${said}` : "Nothing open" }),
|
|
41
|
+
h("p", { class: "muted field-note" }, risk ? "The darker this field, the more serious what is open." : "Nothing open: the field is clear."),
|
|
142
42
|
),
|
|
143
|
-
l.hidden.length ? h("ul", { class: "mini" }, l.hidden.slice(0, 4).map((x) => h("li", {}, sevBadge(x.severity), h("span", { class: "grow" }, `${x.title} · ${x.location || ""}`)))) : null,
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function evidenceCard(bundles, project, ctx) {
|
|
148
|
-
const target = project || state.overview?.projects?.[0]?.key || "";
|
|
149
|
-
const msg = h("p", { class: "muted", "aria-live": "polite" });
|
|
150
|
-
const out = h("div");
|
|
151
|
-
const create = h("button", { type: "button", class: "btn", disabled: !target || !state.overview?.projects?.some((p) => p.key === target) }, "Create bundle");
|
|
152
|
-
create.addEventListener("click", async () => {
|
|
153
|
-
create.disabled = true;
|
|
154
|
-
msg.textContent = "Writing the bundle and hashing every file…";
|
|
155
|
-
try {
|
|
156
|
-
const r = await api.post("/api/evidence/create", { project: target });
|
|
157
|
-
const v = await api.post("/api/evidence/verify", { id: r.id });
|
|
158
|
-
msg.textContent = `${r.path} :: ${v.ok ? "verified" : "verification failed"}`;
|
|
159
|
-
ctx.redrawRight();
|
|
160
|
-
} catch (e) { msg.textContent = e.message; create.disabled = false; }
|
|
161
|
-
});
|
|
162
|
-
const list = h("ul", { class: "mini" }, bundles.map((b) => {
|
|
163
|
-
const res = h("span", { class: "muted" }, b.signed ? "signed" : "unsigned");
|
|
164
|
-
const btn = h("button", { type: "button", class: "btn small" }, "Verify");
|
|
165
|
-
btn.addEventListener("click", async () => {
|
|
166
|
-
btn.disabled = true;
|
|
167
|
-
res.textContent = "hashing…";
|
|
168
|
-
const r = await api.post("/api/evidence/verify", { id: b.id }).catch((e) => ({ ok: false, problems: [e.message], files: [] }));
|
|
169
|
-
res.textContent = r.ok ? "[OK]" : `[!] ${plural(r.problems.length, "problem")}`;
|
|
170
|
-
res.className = r.ok ? "ok" : "bad";
|
|
171
|
-
clear(out).append(r.ok ? null : h("pre", { class: "block" }, r.problems.map((p) => `- ${p}`).join("\n")));
|
|
172
|
-
btn.disabled = false;
|
|
173
|
-
});
|
|
174
|
-
return h("li", {}, h("span", { class: "grow", title: b.target || "" }, `${b.path} · ${ago(b.generatedAt)}`), res, btn);
|
|
175
|
-
}));
|
|
176
|
-
return card("Evidence", plural(bundles.length, "bundle"), false,
|
|
177
|
-
target ? h("div", { class: "actions" },
|
|
178
|
-
h("span", { class: "muted grow" }, shortProject(target)),
|
|
179
|
-
create,
|
|
180
|
-
...["auditor", "owner", "developer"].map((a) => h("a", { class: "btn small", href: `/api/report?${new URLSearchParams({ project: target, audience: a })}`, download: "" }, `${a}.md`)),
|
|
181
|
-
) : null,
|
|
182
|
-
msg,
|
|
183
|
-
bundles.length ? list : empty(EMPTY.evidence),
|
|
184
|
-
out,
|
|
185
|
-
);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function changesCard(env, project) {
|
|
189
|
-
const past = state.runs.past.filter((l) => !project || l.target.repo === project || l.target.url === project);
|
|
190
|
-
const fixes = past.flatMap((l) => l.iterations.flatMap((it) => it.fixes.filter((f) => f.commit).map((f) => ({ ...f, branch: l.branch, at: l.startedAt }))));
|
|
191
|
-
return card("Changes", plural(fixes.length, "fix", "fixes"), false,
|
|
192
|
-
fixes.length ? h("ul", { class: "mini" }, fixes.slice(0, 12).map((f) => h("li", {},
|
|
193
|
-
h("span", { class: "ok" }, "OK"),
|
|
194
|
-
h("span", { class: "grow" }, f.title, h("span", { class: "muted" }, ` · ${f.branch}`)),
|
|
195
|
-
h("code", { class: "muted", title: `git show ${f.commit}` }, f.commit.slice(0, 7))))) : empty(EMPTY.changes),
|
|
196
|
-
env.fixBranches.length ? h("div", {}, h("p", { class: "label" }, "Local fix branches"), h("ul", { class: "mini" }, env.fixBranches.map((b) => h("li", {}, h("code", { class: "grow" }, b))))) : null,
|
|
197
|
-
h("p", { class: "muted" }, "Nothing is pushed. Review with git log --stat main..<branch>, then merge or push it yourself."),
|
|
198
43
|
);
|
|
199
44
|
}
|
|
200
45
|
|
|
@@ -211,9 +56,37 @@ async function findingView(el, ctx, my) {
|
|
|
211
56
|
if (my !== seq) return;
|
|
212
57
|
const x = d.explain || {};
|
|
213
58
|
const section = (label, ...body) => h("section", {}, h("p", { class: "label" }, label), ...body);
|
|
59
|
+
const canFix = d.fix && f.project.startsWith("/");
|
|
60
|
+
if (simple()) {
|
|
61
|
+
// Plain words first, one action, the detail folded away.
|
|
62
|
+
const sure = { confirmed: "Penthera is sure about this one.", likely: "Penthera is fairly sure; worth checking.", "needs-human-review": "Penthera cannot be sure; a person should look." }[d.confidence] || "";
|
|
63
|
+
clear(el).append(
|
|
64
|
+
head("Problem", ctx),
|
|
65
|
+
h("div", { class: "pane-body detail" },
|
|
66
|
+
h("div", {}, h("span", { class: `sev sev-${d.severity}` }, SEV_WORD[d.severity] || d.severity)),
|
|
67
|
+
h("h2", { tabindex: "-1" }, d.title),
|
|
68
|
+
h("div", { class: "plain", "aria-label": "In plain words" },
|
|
69
|
+
d.description ? h("p", {}, h("b", {}, "What is wrong :: "), d.description) : null,
|
|
70
|
+
h("p", {}, h("b", {}, "How sure :: "), sure),
|
|
71
|
+
x.fixText || d.remediation ? h("p", {}, h("b", {}, "What to do :: "), x.fixText || d.remediation) : null),
|
|
72
|
+
canFix
|
|
73
|
+
? h("div", { class: "actions start" }, h("button", { type: "button", class: "btn primary", onclick: () => ctx.newScan("loop", f.project) }, "Fix it for me…"),
|
|
74
|
+
h("p", { class: "muted prose" }, "You will see the change and approve it first."))
|
|
75
|
+
: h("p", { class: "muted prose" }, "Penthera has no automatic fix for this one. The steps above say what to change; a developer can do it in minutes."),
|
|
76
|
+
h("details", { class: "more" }, h("summary", {}, "Show the details"),
|
|
77
|
+
h("dl", { class: "kv" },
|
|
78
|
+
h("dt", {}, "where"), h("dd", {}, d.location || "-"),
|
|
79
|
+
h("dt", {}, "project"), h("dd", {}, f.project)),
|
|
80
|
+
d.evidence ? section("Evidence", h("pre", { class: "block" }, d.evidence)) : null,
|
|
81
|
+
d.fix ? section("The change", diffBlock(d.fix.diff)) : null,
|
|
82
|
+
d.sources?.length ? section("Read more", h("ul", { class: "mini" }, d.sources.map((s) => h("li", {}, h("a", { href: s.url, target: "_blank", rel: "noopener noreferrer" }, s.cite))))) : null),
|
|
83
|
+
),
|
|
84
|
+
);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
214
87
|
clear(el).append(
|
|
215
88
|
head("Finding", ctx),
|
|
216
|
-
h("div", { class: "detail" },
|
|
89
|
+
h("div", { class: "pane-body detail" },
|
|
217
90
|
h("div", {}, sevBadge(d.severity), " ", h("span", { class: "chip" }, d.confidence || "unrated"), " ", d.category ? h("span", { class: "chip" }, d.category) : null),
|
|
218
91
|
h("h2", { tabindex: "-1" }, d.title),
|
|
219
92
|
h("dl", { class: "kv" },
|
|
@@ -230,9 +103,9 @@ async function findingView(el, ctx, my) {
|
|
|
230
103
|
d.remediation ? section("How to fix", h("p", {}, d.remediation)) : null,
|
|
231
104
|
d.sources?.length ? section("Sources", h("ul", { class: "mini" }, d.sources.map((s) => h("li", {}, h("a", { href: s.url, target: "_blank", rel: "noopener noreferrer" }, s.cite))))) : null,
|
|
232
105
|
d.fix ? section("Fix recipe",
|
|
233
|
-
h("p", {}, h("span", { class: "
|
|
106
|
+
h("p", {}, h("span", { class: "chip" }, d.fix.recipe), ` :: ${d.fix.title}`),
|
|
234
107
|
diffBlock(d.fix.diff),
|
|
235
|
-
f.project.startsWith("/") ? h("div", { class: "actions" }, h("button", { type: "button", class: "btn primary", onclick: () =>
|
|
108
|
+
f.project.startsWith("/") ? h("div", { class: "actions" }, h("button", { type: "button", class: "btn primary", onclick: () => ctx.newScan("loop", f.project) }, "Fix it with the loop…")) : null)
|
|
236
109
|
: section("Fix recipe", h("p", { class: "muted" }, "No recipe matches. Fix it by hand, or run the loop with a local model (--fix-model) from the CLI.")),
|
|
237
110
|
),
|
|
238
111
|
);
|
|
@@ -245,12 +118,20 @@ async function settingsView(el, ctx, my) {
|
|
|
245
118
|
if (my !== seq) return;
|
|
246
119
|
const radios = ["dark", "light"].map((t) => {
|
|
247
120
|
const r = h("input", { type: "radio", name: "theme", value: t, checked: ctx.theme.get() === t });
|
|
248
|
-
r.addEventListener("change", () => ctx.
|
|
121
|
+
r.addEventListener("change", () => ctx.setTheme(t));
|
|
249
122
|
return h("label", {}, r, h("span", {}, t === "dark" ? "Dark" : "Light"));
|
|
250
123
|
});
|
|
251
124
|
clear(el).append(
|
|
252
125
|
head("Settings", ctx),
|
|
253
|
-
h("div", { class: "detail" },
|
|
126
|
+
h("div", { class: "pane-body detail" },
|
|
127
|
+
h("section", {}, h("p", { class: "label" }, "Mode"),
|
|
128
|
+
h("div", { class: "seg", role: "radiogroup", "aria-label": "Mode" }, ["simple", "advanced"].map((m) => {
|
|
129
|
+
const r = h("input", { type: "radio", name: "ux", value: m, checked: state.ux === m });
|
|
130
|
+
r.addEventListener("change", () => ctx.setUx(m));
|
|
131
|
+
return h("label", {}, r, h("span", {}, m === "simple" ? "Simple" : "Advanced"));
|
|
132
|
+
})),
|
|
133
|
+
h("p", { class: "muted prose" }, "Simple shows what to fix in plain words. Advanced adds dependencies, AI tooling, history, evidence and the raw detail."),
|
|
134
|
+
h("div", { class: "actions start" }, h("button", { type: "button", class: "btn", onclick: () => ctx.setup() }, "Run setup again…"))),
|
|
254
135
|
h("section", {}, h("p", { class: "label" }, "Theme"), h("div", { class: "seg", role: "radiogroup", "aria-label": "Theme" }, radios),
|
|
255
136
|
h("p", { class: "muted" }, "Remembered in this browser. The first visit follows your system.")),
|
|
256
137
|
h("section", {}, h("p", { class: "label" }, "Where data comes from"),
|
|
@@ -261,10 +142,10 @@ async function settingsView(el, ctx, my) {
|
|
|
261
142
|
h("p", { class: "muted" }, "Only these folders and ~/.penthera are read. Add folders with penthera ui --project <dir>.")),
|
|
262
143
|
h("section", {}, h("p", { class: "label" }, "Egress for scans started here"), h("p", {}, settings.offline ? "Offline: only the target." : "Online: target plus recon sources.")),
|
|
263
144
|
h("section", {}, h("p", { class: "label" }, "Memory chains"),
|
|
264
|
-
mem.projects.length ? h("ul", { class: "mini" }, mem.projects.map((p) => h("li", {}, h("span", { class: p.chain === "ok" ? "
|
|
145
|
+
mem.projects.length ? h("ul", { class: "mini" }, mem.projects.map((p) => h("li", {}, h("span", { class: p.chain === "ok" ? "t1" : "bad" }, p.chain === "ok" ? "✓" : "[!]"), h("span", { class: "grow" }, p.label), h("span", { class: "muted" }, `${p.events} events`)))) : empty(EMPTY.memory)),
|
|
265
146
|
h("section", {}, h("p", { class: "label" }, "Keys"), h("dl", { class: "kv" }, KEYS.flatMap(([k, v]) => [h("dt", {}, h("kbd", {}, k)), h("dd", {}, v)]))),
|
|
266
147
|
h("section", {}, h("p", { class: "label" }, "Session"),
|
|
267
|
-
h("p", { class: "muted" }, `127.0.0.1 only · one-time link swapped for
|
|
148
|
+
h("p", { class: "muted" }, `127.0.0.1 only · one-time link swapped for a session kept in this tab only · Host and Origin checked · strict CSP · ${state.meta.demo ? "demo data" : "local reports"}`)),
|
|
268
149
|
),
|
|
269
150
|
);
|
|
270
151
|
}
|
|
@@ -1,17 +1,29 @@
|
|
|
1
|
-
// One store for the single view. Renderers subscribe and redraw their
|
|
1
|
+
// One store for the single view. Renderers subscribe and redraw their window.
|
|
2
2
|
export const state = {
|
|
3
3
|
meta: null,
|
|
4
4
|
overview: null,
|
|
5
5
|
findings: [],
|
|
6
6
|
runs: { live: [], past: [] },
|
|
7
7
|
sel: { kind: "all", key: null }, // all | project | run | past
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
tab: "findings", // findings | deps | ai | history | proof (for all / project)
|
|
9
|
+
finding: null, // { id, project } open in the inspector
|
|
10
|
+
right: "none", // none | finding | settings
|
|
10
11
|
query: "",
|
|
11
|
-
|
|
12
|
-
|
|
12
|
+
filter: null, // { severity, confidence } from the triage matrix, advanced only
|
|
13
|
+
pending: null, // the live run's diff waiting for a decision, if any
|
|
14
|
+
ux: "simple", // simple | advanced, chosen in setup, remembered per browser
|
|
13
15
|
};
|
|
14
16
|
|
|
17
|
+
export const simple = () => state.ux === "simple";
|
|
18
|
+
|
|
19
|
+
export const TABS = [
|
|
20
|
+
{ id: "findings", label: "Findings", key: "1" },
|
|
21
|
+
{ id: "deps", label: "Dependencies", key: "2" },
|
|
22
|
+
{ id: "ai", label: "AI tooling", key: "3" },
|
|
23
|
+
{ id: "history", label: "History", key: "4" },
|
|
24
|
+
{ id: "proof", label: "Proof", key: "5" },
|
|
25
|
+
];
|
|
26
|
+
|
|
15
27
|
const subs = new Set();
|
|
16
28
|
export function on(fn) { subs.add(fn); return () => subs.delete(fn); }
|
|
17
29
|
export function set(patch) {
|
|
@@ -33,3 +45,22 @@ export function folderFor(key) {
|
|
|
33
45
|
if (key && key.startsWith("/")) return key;
|
|
34
46
|
return state.meta?.projects?.[0] || "";
|
|
35
47
|
}
|
|
48
|
+
|
|
49
|
+
/** The project the current view is about, if any (a run or past run counts as its project). */
|
|
50
|
+
export function currentProject() {
|
|
51
|
+
const s = state.sel;
|
|
52
|
+
if (s.kind === "project") return s.key;
|
|
53
|
+
if (s.kind === "past") return s.past?.target?.repo || s.past?.target?.url || null;
|
|
54
|
+
if (s.kind === "run") return s.run?.repo || s.run?.url || null;
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The live run that is still going, if any. */
|
|
59
|
+
export const liveRun = () => state.runs.live.find((r) => r.status === "running") || null;
|
|
60
|
+
|
|
61
|
+
/** Severity counts for what is on screen: the selected project, or all of them. */
|
|
62
|
+
export function countsInView() {
|
|
63
|
+
const key = currentProject();
|
|
64
|
+
if (key) return projectOf(key)?.counts || null;
|
|
65
|
+
return state.overview?.mix || null;
|
|
66
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
<svg width="279" height="63" viewBox="0 0 279 63" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M13.0292 54.9484L20.4241 57.491H0L7.39494 54.9484V3.53139L0 0.988789H16.7619C25.5654 0.988789 31.0588 5.36771 31.0588 12.0774C31.0588 21.9652 20.4241 27.0504 13.0292 27.5448V54.9484ZM13.0292 1.69507V26.8386C17.4661 26.4854 25.0019 24.0135 25.0019 13.9137C25.0019 7.06278 20.7763 1.69507 13.0292 1.69507Z" fill="currentColor"/>
|
|
3
|
+
<path d="M46.8324 56.7848C53.7344 56.7848 58.453 52.8296 61.4815 42.1648L62.6083 38.2096H63.0309L58.1713 57.491H29.4367L36.8316 54.9484V3.53139L29.4367 0.988789H58.1009L61.1997 18.7164H60.7772L60.0025 14.6906C58.2418 5.50896 53.9457 1.69507 46.0577 1.69507H42.4659V23.3778H57.5375V23.5191L42.4659 26.9092V56.7848H46.8324Z" fill="currentColor"/>
|
|
4
|
+
<path d="M101.382 0.988789L93.9873 3.46076L93.2126 58.6917L68.2811 4.52018L69.1262 54.9484L76.5916 57.491H59.4071L66.8725 54.9484L67.6472 3.60202L59.8297 0.988789H73.211L92.79 44.7074L91.9449 3.53139L84.5499 0.988789H101.382Z" fill="currentColor"/>
|
|
5
|
+
<path d="M129.767 0.988789L132.866 18.7164H132.444L131.599 14.1962C129.838 4.73206 124.415 1.69507 119.415 1.69507H118.288V54.9484L125.823 57.491H105.118L112.653 54.9484V1.69507H111.808C107.09 1.69507 101.385 4.66143 99.6243 14.1962L98.7791 18.7164H98.3566L101.455 0.988789H129.767Z" fill="currentColor"/>
|
|
6
|
+
<path d="M137.934 3.53139L130.539 0.988789H150.964L143.569 3.53139V25.7791H160.119V3.53139L152.724 0.988789H173.148L165.753 3.53139V54.9484L173.148 57.491H152.724L160.119 54.9484V26.4854H143.569V54.9484L150.964 57.491H130.539L137.934 54.9484V3.53139Z" fill="currentColor"/>
|
|
7
|
+
<path d="M188.032 56.7848C194.934 56.7848 199.653 52.8296 202.681 42.1648L203.808 38.2096H204.231L199.371 57.491H170.637L178.032 54.9484V3.53139L170.637 0.988789H199.301L202.4 18.7164H201.977L201.202 14.6906C199.442 5.50896 195.146 1.69507 187.258 1.69507H183.666V23.3778H198.737V23.5191L183.666 26.9092V56.7848H188.032Z" fill="currentColor"/>
|
|
8
|
+
<path d="M214.34 54.9484L221.735 57.491H201.311L208.706 54.9484V3.53139L201.311 0.988789H218.073C226.877 0.988789 232.37 5.15583 232.37 11.0179C232.37 17.3744 227.018 21.4002 221.383 23.3072L235.891 63H235.68L214.34 24.6491V54.9484ZM214.34 1.69507V24.0135C221.172 23.8016 226.313 19.2814 226.313 12.5011C226.313 6.42713 222.088 1.69507 214.34 1.69507Z" fill="currentColor"/>
|
|
9
|
+
<path d="M258.576 57.491L265.971 55.0191L259.21 31.5H247.589L241.814 55.0191L249.279 57.491H232.518L239.49 54.8778L255.195 0L271.605 54.9484L279 57.491H258.576ZM247.8 30.7937H258.998L252.871 9.88789L247.8 30.7937Z" fill="currentColor"/>
|
|
10
|
+
</svg>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg width="140" height="280" viewBox="0 0 140 280" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path d="M58.7302 243.645L108.182 280L58.7302 255.125H0L33.3333 243.645V11.4806L0 0H75.5556C115.238 0 140 19.7722 140 50.0683C140 94.7152 92.0635 117.676 58.7302 119.909V243.645ZM58.7302 3.18906V116.72C78.7302 115.125 112.698 103.964 112.698 58.3599C112.698 27.4259 93.6508 3.18906 58.7302 3.18906Z" fill="currentColor"/>
|
|
3
|
+
</svg>
|