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.
Files changed (42) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/README.md +158 -301
  3. package/bin/scan.js +2 -2
  4. package/docs/METHODOLOGY.md +236 -182
  5. package/docs/commands/README.md +2 -1
  6. package/docs/commands/init.md +114 -0
  7. package/docs/commands/scan.md +1 -1
  8. package/docs/compare/github.md +1 -1
  9. package/docs/dashboard.md +95 -152
  10. package/docs/examples/github-actions-scan.yml +1 -1
  11. package/docs/internal-audit.md +1 -1
  12. package/docs/plan.md +3 -2
  13. package/docs/references.bib +43 -33
  14. package/docs/references.md +92 -51
  15. package/lib/whitebox/frameworks.js +36 -0
  16. package/package.json +1 -1
  17. package/skills/penthera/SKILL.md +19 -5
  18. package/src/cli/deps.js +1 -1
  19. package/src/cli/detect.js +125 -0
  20. package/src/cli/onboarding.js +428 -6
  21. package/src/cli/prompt.js +42 -0
  22. package/src/cli.js +18 -5
  23. package/src/ui/api/data.js +30 -0
  24. package/src/ui/api/demo.js +13 -0
  25. package/src/ui/api/routes.js +1 -0
  26. package/src/ui/static/app.css +286 -560
  27. package/src/ui/static/index.html +8 -39
  28. package/src/ui/static/js/app.js +105 -107
  29. package/src/ui/static/js/center.js +388 -0
  30. package/src/ui/static/js/copy.js +36 -62
  31. package/src/ui/static/js/dom.js +0 -38
  32. package/src/ui/static/js/left.js +83 -0
  33. package/src/ui/static/js/right.js +270 -0
  34. package/src/ui/static/js/state.js +35 -0
  35. package/src/ui/static/js/views/ai.js +0 -68
  36. package/src/ui/static/js/views/deps.js +0 -96
  37. package/src/ui/static/js/views/evidence.js +0 -115
  38. package/src/ui/static/js/views/findings.js +0 -165
  39. package/src/ui/static/js/views/history.js +0 -92
  40. package/src/ui/static/js/views/overview.js +0 -184
  41. package/src/ui/static/js/views/run.js +0 -349
  42. package/src/ui/static/js/views/settings.js +0 -67
package/src/cli.js CHANGED
@@ -51,10 +51,11 @@ function shouldRunOnboarding(positionals, opts) {
51
51
  }
52
52
 
53
53
  const HELP = `
54
- ${bold("penthera")} v${pkg.version} \u2014 lightweight security scanner
54
+ ${bold("penthera")} v${pkg.version}: Cyber Security Agent. Open-source security for the apps and AI systems you ship
55
55
 
56
56
  ${dim("Usage")}
57
- $ penthera Interactive setup (TTY, no args needed)
57
+ $ penthera Setup wizard: detect, check, set up (TTY, no args)
58
+ $ penthera init [--yes] [--dry-run] The same wizard, also non-interactive
58
59
  $ penthera <url> Scan a live URL (black-box)
59
60
  $ penthera --repo <path> Scan a local repo (white-box)
60
61
  $ penthera <url> --repo <path> Full scan (both)
@@ -168,7 +169,20 @@ const HELP = `
168
169
  function bold(s) { return `\x1b[1m${s}\x1b[0m`; }
169
170
  function dim(s) { return `\x1b[2m${s}\x1b[0m`; }
170
171
 
172
+ /** `penthera init ...`, or bare `penthera --yes` / `--dry-run`: the setup wizard. */
173
+ function initArgs(argv) {
174
+ if (argv[0] === "init") return argv.slice(1);
175
+ if (argv.length && argv.every((a) => ["--yes", "-y", "--dry-run"].includes(a))) return argv;
176
+ return null;
177
+ }
178
+
171
179
  export async function run() {
180
+ const init = initArgs(process.argv.slice(2));
181
+ if (init) {
182
+ const { runInit } = await import("./cli/onboarding.js");
183
+ process.exit(await runInit(init, { version: pkg.version }));
184
+ }
185
+
172
186
  if (process.argv[2] === "memory") {
173
187
  const { runMemoryCommand } = await import("./cli/memory.js");
174
188
  process.exit(await runMemoryCommand(process.argv.slice(3)));
@@ -389,9 +403,8 @@ export async function run() {
389
403
  }
390
404
 
391
405
  if (shouldRunOnboarding(positionals, opts)) {
392
- const { runOnboarding } = await import("./cli/onboarding.js");
393
- await runOnboarding();
394
- return;
406
+ const { runInit } = await import("./cli/onboarding.js");
407
+ process.exit(await runInit([], { version: pkg.version }));
395
408
  }
396
409
 
397
410
  // `penthera agents [path]`: offline audit of skills, MCP servers, hooks, plugins.
@@ -15,6 +15,7 @@ import { referencesFor } from "../../../lib/references.js";
15
15
  import { planFixes, redactPlan } from "../../../lib/fix/index.js";
16
16
  import { verifyEvidence, MANIFEST, SIGNATURE, SCHEMA as EVIDENCE_SCHEMA } from "../../report/evidence.js";
17
17
  import { writeEvidence } from "../../cli/report-outputs.js";
18
+ import { createGit } from "../../agent/git.js";
18
19
  import { formatAudienceReport, AUDIENCES } from "../../report/audience.js";
19
20
  import {
20
21
  classify, buildOverview, buildFindings, locateFinding, findingDetail, buildDeps, buildAi, memoryStats, loopSummary,
@@ -216,6 +217,35 @@ export function createDataSource({ projects, reportsDir, home, memoryRoot, setti
216
217
  return { ...detail, id, project, sources, fix, file: hit.report.name, explain: explainFinding(hit.finding, hit.report.data) };
217
218
  },
218
219
 
220
+ /**
221
+ * The Environment card: which folder a project maps to, its git branch and
222
+ * HEAD, local penthera/fix-* branches, and the hosts its latest scan
223
+ * contacted. Git is read through the agent's hardened runner
224
+ * (src/agent/git.js: allow-listed subcommands, hooks and fsmonitor off).
225
+ */
226
+ env(project) {
227
+ let folder = null;
228
+ if (typeof project === "string" && project.startsWith("/")) {
229
+ try { folder = confineDir(project, projectRoots); } catch { folder = null; }
230
+ }
231
+ const out = { folder, branch: null, head: null, fixBranches: [], hosts: [], blocked: [] };
232
+ if (folder) {
233
+ const git = createGit(folder);
234
+ out.branch = git.tryRun("symbolic-ref", "--quiet", "--short", "HEAD")?.trim() || null;
235
+ out.head = git.tryRun("rev-parse", "--short", "HEAD")?.trim() || null;
236
+ const refs = git.tryRun("show-ref", "--heads") || "";
237
+ out.fixBranches = refs.split("\n").map((l) => l.split(" ")[1] || "").filter((r) => r.startsWith("refs/heads/penthera/fix-"))
238
+ .map((r) => r.replace("refs/heads/", "")).slice(-10).reverse();
239
+ }
240
+ const latest = latestScan(scan().scans, project);
241
+ const e = latest?.data?.egress;
242
+ if (e) {
243
+ out.hosts = (e.hosts || []).slice(0, 12).map((h) => ({ host: String(h.host).slice(0, 255), requests: h.requests || 0, thirdParty: (e.thirdPartyHosts || []).some((t) => t.host === h.host) }));
244
+ out.blocked = (e.blocked || []).slice(0, 8).map((h) => ({ host: String(h.host).slice(0, 255), requests: h.requests || 0 }));
245
+ }
246
+ return out;
247
+ },
248
+
219
249
  history(project, a, b) {
220
250
  return buildScanHistory(scan().scans, project, a, b);
221
251
  },
@@ -307,6 +307,19 @@ export function createDemoSource({ settings, version = "dev" }) {
307
307
  };
308
308
  },
309
309
  history: (project, a, b) => buildScanHistory(scans, project, a, b),
310
+ env(project) {
311
+ const latest = latestScan(scans, project);
312
+ const e = latest?.data?.egress;
313
+ const isRepo = String(project).startsWith("/");
314
+ return {
315
+ folder: isRepo ? project : null,
316
+ branch: isRepo ? "main" : null,
317
+ head: isRepo ? "4be9c1f" : null,
318
+ fixBranches: isRepo ? loops.map((l) => l.loopRun.branch.fix) : [],
319
+ hosts: (e?.hosts || []).map((h) => ({ ...h, thirdParty: (e.thirdPartyHosts || []).some((t) => t.host === h.host) })),
320
+ blocked: e?.blocked || [],
321
+ };
322
+ },
310
323
  createEvidence(project) {
311
324
  const latest = latestScan(scans, project);
312
325
  if (!latest) return null;
@@ -75,6 +75,7 @@ export function createApi({ source, runs, version, settings }) {
75
75
  if (!r) throw new RunError("No scan report for that project yet. Run a scan first.", 404);
76
76
  return r;
77
77
  }],
78
+ ["GET", /^\/api\/env$/, (_m, q) => source.env(String(q.get("project") || ""))],
78
79
  ["GET", /^\/api\/deps$/, () => source.deps()],
79
80
  ["GET", /^\/api\/ai$/, () => source.ai()],
80
81
  ["GET", /^\/api\/memory$/, () => ({ projects: source.memory() })],