penthera 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/CHANGELOG.md +168 -0
  2. package/LICENSE +21 -0
  3. package/README.md +561 -0
  4. package/SECURITY.md +31 -0
  5. package/bin/penthera.js +3 -0
  6. package/bin/scan.js +11 -0
  7. package/docs/index.md +60 -0
  8. package/docs/owasp-wstg-coverage.md +79 -0
  9. package/lib/adaptive-probes.js +334 -0
  10. package/lib/attack-surface.js +149 -0
  11. package/lib/audit-loop.js +170 -0
  12. package/lib/blackbox/adaptive-scan.js +137 -0
  13. package/lib/blackbox/client-auth.js +60 -0
  14. package/lib/blackbox/email-dns.js +135 -0
  15. package/lib/blackbox/headers.js +91 -0
  16. package/lib/blackbox/idor.js +113 -0
  17. package/lib/blackbox/jwt.js +73 -0
  18. package/lib/blackbox/oauth.js +50 -0
  19. package/lib/blackbox/openapi.js +227 -0
  20. package/lib/compliance/incident.js +169 -0
  21. package/lib/compliance/index.js +163 -0
  22. package/lib/compliance/policy-pack.js +293 -0
  23. package/lib/compliance/questionnaire.js +113 -0
  24. package/lib/compliance/readiness.js +229 -0
  25. package/lib/compliance/self-assessment.js +66 -0
  26. package/lib/crawler.js +257 -0
  27. package/lib/fuzzer.js +233 -0
  28. package/lib/injections.js +676 -0
  29. package/lib/jurisdictions/nl.json +48 -0
  30. package/lib/knowledge-graph.js +232 -0
  31. package/lib/machine.js +814 -0
  32. package/lib/nuclei-loader.js +283 -0
  33. package/lib/owasp-wstg.js +204 -0
  34. package/lib/params.js +235 -0
  35. package/lib/plugins.js +70 -0
  36. package/lib/provenance.js +82 -0
  37. package/lib/recon.js +193 -0
  38. package/lib/remediation/index.js +219 -0
  39. package/lib/retirejs.js +239 -0
  40. package/lib/supply-chain.js +131 -0
  41. package/lib/templates.js +364 -0
  42. package/lib/tls.js +145 -0
  43. package/lib/whitebox/frameworks.js +139 -0
  44. package/lib/whitebox/sbom.js +144 -0
  45. package/lib/whitebox/secrets.js +111 -0
  46. package/package.json +76 -0
  47. package/pentest.config.example.js +82 -0
  48. package/scripts/package-skill.sh +16 -0
  49. package/skills/penthera/SKILL.md +272 -0
  50. package/skills/penthera/references/authorization.md +36 -0
  51. package/skills/penthera/references/output-and-ci.md +109 -0
  52. package/skills/penthera/references/profiles-and-flags.md +74 -0
  53. package/skills/penthera/references/remediation.md +267 -0
  54. package/skills/penthera/references/reporting.md +113 -0
  55. package/skills/penthera/references/secure-defaults.md +41 -0
  56. package/skills/penthera/references/troubleshooting.md +100 -0
  57. package/skills/penthera/scripts/preflight.sh +66 -0
  58. package/skills/penthera/scripts/validate-report.mjs +53 -0
  59. package/skills/penthera/scripts/validate-report.sh +8 -0
  60. package/src/cli/ansi.js +70 -0
  61. package/src/cli/baseline.js +37 -0
  62. package/src/cli/merge-results.js +71 -0
  63. package/src/cli/onboarding.js +216 -0
  64. package/src/cli/profiles.js +53 -0
  65. package/src/cli/prompt.js +64 -0
  66. package/src/cli/run-scan.js +152 -0
  67. package/src/cli.js +536 -0
  68. package/src/report/markdown.js +182 -0
  69. package/src/reporter.js +512 -0
  70. package/src/scan-repo.js +205 -0
  71. package/src/scan-url.js +563 -0
  72. package/src/utils/auth.js +13 -0
  73. package/src/utils/http.js +38 -0
  74. package/src/utils/url.js +54 -0
@@ -0,0 +1,48 @@
1
+ {
2
+ "id": "nl",
3
+ "name": "Netherlands",
4
+ "law": "Cyberbeveiligingswet (NIS2 transposition)",
5
+ "framework": "nis2",
6
+ "language": "en",
7
+ "authorities": {
8
+ "supervisor": "RDI, Rijksinspectie Digitale Infrastructuur",
9
+ "csirt": "NCSC-NL (or your sectoral CSIRT)",
10
+ "note": "The competent authority and CSIRT depend on your sector. Confirm which one applies to you before relying on this."
11
+ },
12
+ "incidentDeadlines": [
13
+ {
14
+ "id": "early-warning",
15
+ "within": "24 hours",
16
+ "from": "awareness",
17
+ "offsetHours": 24,
18
+ "of": "becoming aware of a significant incident",
19
+ "to": "your CSIRT / competent authority",
20
+ "content": "Early warning: whether the incident is suspected to be unlawful/malicious and whether it could have cross-border impact."
21
+ },
22
+ {
23
+ "id": "notification",
24
+ "within": "72 hours",
25
+ "from": "awareness",
26
+ "offsetHours": 72,
27
+ "of": "becoming aware of a significant incident",
28
+ "to": "your CSIRT / competent authority",
29
+ "content": "Incident notification: initial assessment, severity and impact, and any indicators of compromise."
30
+ },
31
+ {
32
+ "id": "final-report",
33
+ "within": "1 month",
34
+ "from": "notification",
35
+ "offsetMonths": 1,
36
+ "of": "the incident notification",
37
+ "to": "your CSIRT / competent authority",
38
+ "content": "Final report: detailed description, root cause, applied and ongoing mitigations, and any cross-border impact."
39
+ }
40
+ ],
41
+ "penalties": "Up to €10M or 2% of global annual turnover. Management can be held personally accountable.",
42
+ "pointers": [
43
+ "Determine whether you are an essential or important entity, and your sector's competent authority.",
44
+ "Register with your competent authority if in scope.",
45
+ "Keep evidence of your risk-management measures ready to produce if audited."
46
+ ],
47
+ "disclaimer": "Penthera provides readiness, evidence, and self-assessment tooling only. It is not legal advice and does not certify or guarantee compliance. Confirm your obligations with your competent authority (RDI) or a qualified advisor."
48
+ }
@@ -0,0 +1,232 @@
1
+ /**
2
+ * Security Knowledge Graph (Portable)
3
+ *
4
+ * Builds a graph of the application's security posture:
5
+ * Nodes = routes, auth mechanisms, data stores, external services, findings
6
+ * Edges = trust boundaries, data flows, attack paths
7
+ *
8
+ * Enables chain discovery, impact analysis, coverage tracking, and historical diffs.
9
+ */
10
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "fs";
11
+ import { join } from "path";
12
+
13
+ const RESULTS_DIR = join(process.cwd(), ".security-results");
14
+ const GRAPH_FILE = join(RESULTS_DIR, "knowledge-graph.json");
15
+ const HISTORY_FILE = join(RESULTS_DIR, "findings-history.json");
16
+
17
+ export class SecurityKnowledgeGraph {
18
+ constructor() {
19
+ this.nodes = new Map();
20
+ this.edges = [];
21
+ this.findings = [];
22
+ this.probeResults = [];
23
+ }
24
+
25
+ // ── Node management ─────────────────────────────────────────────────────
26
+
27
+ addRoute(route) {
28
+ const id = `route:${route.url}`;
29
+ this.nodes.set(id, { type: "route", data: route, findings: [] });
30
+
31
+ for (const auth of route.auth) {
32
+ const authId = `auth:${auth}`;
33
+ if (!this.nodes.has(authId)) {
34
+ this.nodes.set(authId, { type: "auth", data: { mechanism: auth }, findings: [] });
35
+ }
36
+ this.edges.push({ from: id, to: authId, type: "protected-by", label: `requires ${auth}` });
37
+ }
38
+
39
+ for (const svc of route.externalCalls) {
40
+ const svcId = `service:${svc}`;
41
+ if (!this.nodes.has(svcId)) {
42
+ this.nodes.set(svcId, { type: "service", data: { name: svc }, findings: [] });
43
+ }
44
+ this.edges.push({ from: id, to: svcId, type: "calls", label: `calls ${svc}` });
45
+ }
46
+
47
+ return id;
48
+ }
49
+
50
+ // ── Finding management ──────────────────────────────────────────────────
51
+
52
+ addFinding({ severity, category, title, detail, routeUrl, chain = [], probeData = {} }) {
53
+ const finding = {
54
+ id: `F-${this.findings.length + 1}`,
55
+ severity,
56
+ category,
57
+ title,
58
+ detail,
59
+ routeUrl,
60
+ chain,
61
+ probeData,
62
+ timestamp: new Date().toISOString(),
63
+ };
64
+ this.findings.push(finding);
65
+
66
+ const routeId = `route:${routeUrl}`;
67
+ if (this.nodes.has(routeId)) {
68
+ this.nodes.get(routeId).findings.push(finding.id);
69
+ }
70
+ return finding;
71
+ }
72
+
73
+ addProbeResult(result) {
74
+ this.probeResults.push({ ...result, timestamp: new Date().toISOString() });
75
+ return result;
76
+ }
77
+
78
+ // ── Chain Discovery ─────────────────────────────────────────────────────
79
+
80
+ discoverChains() {
81
+ const chains = [];
82
+
83
+ // Pattern 1: Public endpoint + user input + external service
84
+ const publicRoutes = this.getRoutesByAuth("public");
85
+ for (const route of publicRoutes) {
86
+ if (route.data.inputs.length > 0 && route.data.externalCalls.length > 0) {
87
+ chains.push({
88
+ type: "public-input-to-external",
89
+ severity: "medium",
90
+ path: [route.data.url],
91
+ description: `Public endpoint ${route.data.url} accepts input (${route.data.inputs.join(", ")}) → calls ${route.data.externalCalls.join(", ")}`,
92
+ });
93
+ }
94
+ }
95
+
96
+ // Pattern 2: Parameterised routes without transaction protection
97
+ const paramRoutes = [...this.nodes.values()].filter(n => n.type === "route" && n.data.hasParams);
98
+ for (const route of paramRoutes) {
99
+ if (!route.data.securityPatterns.includes("transactional")) {
100
+ chains.push({
101
+ type: "potential-idor",
102
+ severity: "medium",
103
+ path: [route.data.url],
104
+ description: `Parameterised route ${route.data.url}, verify ownership check`,
105
+ });
106
+ }
107
+ }
108
+
109
+ // Pattern 3: Error leak chain
110
+ const leakyRoutes = [...this.nodes.values()]
111
+ .filter(n => n.type === "route" && n.data.securityPatterns.includes("RISK:error-leak"));
112
+ if (leakyRoutes.length > 0) {
113
+ chains.push({
114
+ type: "error-leak-chain",
115
+ severity: "low",
116
+ path: leakyRoutes.map(r => r.data.url),
117
+ description: `${leakyRoutes.length} routes may leak error details → attacker learns stack → targeted exploitation`,
118
+ });
119
+ }
120
+
121
+ // Pattern 4: Rate-limit-free public endpoints
122
+ const unlimitedPublic = publicRoutes.filter(r => !r.data.auth.includes("rate-limited"));
123
+ for (const route of unlimitedPublic) {
124
+ if (route.data.externalCalls.length > 0) {
125
+ chains.push({
126
+ type: "no-rate-limit-abuse",
127
+ severity: "medium",
128
+ path: [route.data.url],
129
+ description: `Public ${route.data.url} has no rate limit, calls ${route.data.externalCalls.join(", ")}, abuse/cost vector`,
130
+ });
131
+ }
132
+ }
133
+
134
+ // Pattern 5: Auth confusion on mixed-auth routes
135
+ const mixedAuth = [...this.nodes.values()]
136
+ .filter(n => n.type === "route" && n.data.auth.length > 1 && !n.data.auth.includes("public"));
137
+ for (const route of mixedAuth) {
138
+ chains.push({
139
+ type: "auth-confusion",
140
+ severity: "low",
141
+ path: [route.data.url],
142
+ description: `${route.data.url} accepts multiple auth (${route.data.auth.join(" + ")}), verify each path`,
143
+ });
144
+ }
145
+
146
+ // Pattern 6: Critical chain, auth bypass + IDOR
147
+ const authBypass = this.findings.filter(f => f.category === "auth-bypass");
148
+ const idor = this.findings.filter(f => f.category === "idor");
149
+ if (authBypass.length > 0 && idor.length > 0) {
150
+ chains.push({
151
+ type: "auth-bypass-to-idor",
152
+ severity: "critical",
153
+ path: [...authBypass.map(f => f.routeUrl), ...idor.map(f => f.routeUrl)],
154
+ description: `Auth bypass on ${authBypass[0].routeUrl} + IDOR on ${idor[0].routeUrl} = cross-user access`,
155
+ });
156
+ }
157
+
158
+ return chains;
159
+ }
160
+
161
+ // ── Queries ─────────────────────────────────────────────────────────────
162
+
163
+ getRoutesByAuth(authType) {
164
+ return [...this.nodes.values()].filter(n => n.type === "route" && n.data.auth.includes(authType));
165
+ }
166
+
167
+ // ── Persistence ─────────────────────────────────────────────────────────
168
+
169
+ save() {
170
+ mkdirSync(RESULTS_DIR, { recursive: true });
171
+ const serializable = {
172
+ timestamp: new Date().toISOString(),
173
+ nodes: Object.fromEntries(this.nodes),
174
+ edges: this.edges,
175
+ findings: this.findings,
176
+ probeResults: this.probeResults,
177
+ chains: this.discoverChains(),
178
+ stats: this.getStats(),
179
+ };
180
+ writeFileSync(GRAPH_FILE, JSON.stringify(serializable, null, 2));
181
+
182
+ const history = this.loadHistory();
183
+ history.push({
184
+ timestamp: serializable.timestamp,
185
+ findingCount: this.findings.length,
186
+ chainCount: serializable.chains.length,
187
+ routeCount: [...this.nodes.values()].filter(n => n.type === "route").length,
188
+ findings: this.findings.map(f => ({ id: f.id, severity: f.severity, category: f.category, title: f.title, routeUrl: f.routeUrl })),
189
+ });
190
+ writeFileSync(HISTORY_FILE, JSON.stringify(history.slice(-50), null, 2));
191
+ return serializable;
192
+ }
193
+
194
+ loadHistory() {
195
+ try {
196
+ if (existsSync(HISTORY_FILE)) return JSON.parse(readFileSync(HISTORY_FILE, "utf-8"));
197
+ } catch { /* ignore */ }
198
+ return [];
199
+ }
200
+
201
+ diffWithLastRun() {
202
+ const history = this.loadHistory();
203
+ if (history.length === 0) return { new: this.findings, fixed: [], recurring: [] };
204
+ const last = history[history.length - 1];
205
+ const lastTitles = new Set(last.findings.map(f => `${f.category}:${f.routeUrl}`));
206
+ const currentTitles = new Set(this.findings.map(f => `${f.category}:${f.routeUrl}`));
207
+ return {
208
+ new: this.findings.filter(f => !lastTitles.has(`${f.category}:${f.routeUrl}`)),
209
+ fixed: last.findings.filter(f => !currentTitles.has(`${f.category}:${f.routeUrl}`)),
210
+ recurring: this.findings.filter(f => lastTitles.has(`${f.category}:${f.routeUrl}`)),
211
+ };
212
+ }
213
+
214
+ getStats() {
215
+ const routes = [...this.nodes.values()].filter(n => n.type === "route");
216
+ return {
217
+ totalRoutes: routes.length,
218
+ publicRoutes: routes.filter(r => r.data.auth.includes("public")).length,
219
+ userRoutes: routes.filter(r => r.data.auth.includes("user")).length,
220
+ adminRoutes: routes.filter(r => r.data.auth.includes("admin")).length,
221
+ cronRoutes: routes.filter(r => r.data.auth.includes("cron")).length,
222
+ routesWithParams: routes.filter(r => r.data.hasParams).length,
223
+ routesWithExternalCalls: routes.filter(r => r.data.externalCalls.length > 0).length,
224
+ riskyRoutes: routes.filter(r => r.data.securityPatterns.some(p => p.startsWith("RISK:"))).length,
225
+ totalFindings: this.findings.length,
226
+ criticalFindings: this.findings.filter(f => f.severity === "critical").length,
227
+ highFindings: this.findings.filter(f => f.severity === "high").length,
228
+ mediumFindings: this.findings.filter(f => f.severity === "medium").length,
229
+ lowFindings: this.findings.filter(f => f.severity === "low").length,
230
+ };
231
+ }
232
+ }