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.
- package/CHANGELOG.md +168 -0
- package/LICENSE +21 -0
- package/README.md +561 -0
- package/SECURITY.md +31 -0
- package/bin/penthera.js +3 -0
- package/bin/scan.js +11 -0
- package/docs/index.md +60 -0
- package/docs/owasp-wstg-coverage.md +79 -0
- package/lib/adaptive-probes.js +334 -0
- package/lib/attack-surface.js +149 -0
- package/lib/audit-loop.js +170 -0
- package/lib/blackbox/adaptive-scan.js +137 -0
- package/lib/blackbox/client-auth.js +60 -0
- package/lib/blackbox/email-dns.js +135 -0
- package/lib/blackbox/headers.js +91 -0
- package/lib/blackbox/idor.js +113 -0
- package/lib/blackbox/jwt.js +73 -0
- package/lib/blackbox/oauth.js +50 -0
- package/lib/blackbox/openapi.js +227 -0
- package/lib/compliance/incident.js +169 -0
- package/lib/compliance/index.js +163 -0
- package/lib/compliance/policy-pack.js +293 -0
- package/lib/compliance/questionnaire.js +113 -0
- package/lib/compliance/readiness.js +229 -0
- package/lib/compliance/self-assessment.js +66 -0
- package/lib/crawler.js +257 -0
- package/lib/fuzzer.js +233 -0
- package/lib/injections.js +676 -0
- package/lib/jurisdictions/nl.json +48 -0
- package/lib/knowledge-graph.js +232 -0
- package/lib/machine.js +814 -0
- package/lib/nuclei-loader.js +283 -0
- package/lib/owasp-wstg.js +204 -0
- package/lib/params.js +235 -0
- package/lib/plugins.js +70 -0
- package/lib/provenance.js +82 -0
- package/lib/recon.js +193 -0
- package/lib/remediation/index.js +219 -0
- package/lib/retirejs.js +239 -0
- package/lib/supply-chain.js +131 -0
- package/lib/templates.js +364 -0
- package/lib/tls.js +145 -0
- package/lib/whitebox/frameworks.js +139 -0
- package/lib/whitebox/sbom.js +144 -0
- package/lib/whitebox/secrets.js +111 -0
- package/package.json +76 -0
- package/pentest.config.example.js +82 -0
- package/scripts/package-skill.sh +16 -0
- package/skills/penthera/SKILL.md +272 -0
- package/skills/penthera/references/authorization.md +36 -0
- package/skills/penthera/references/output-and-ci.md +109 -0
- package/skills/penthera/references/profiles-and-flags.md +74 -0
- package/skills/penthera/references/remediation.md +267 -0
- package/skills/penthera/references/reporting.md +113 -0
- package/skills/penthera/references/secure-defaults.md +41 -0
- package/skills/penthera/references/troubleshooting.md +100 -0
- package/skills/penthera/scripts/preflight.sh +66 -0
- package/skills/penthera/scripts/validate-report.mjs +53 -0
- package/skills/penthera/scripts/validate-report.sh +8 -0
- package/src/cli/ansi.js +70 -0
- package/src/cli/baseline.js +37 -0
- package/src/cli/merge-results.js +71 -0
- package/src/cli/onboarding.js +216 -0
- package/src/cli/profiles.js +53 -0
- package/src/cli/prompt.js +64 -0
- package/src/cli/run-scan.js +152 -0
- package/src/cli.js +536 -0
- package/src/report/markdown.js +182 -0
- package/src/reporter.js +512 -0
- package/src/scan-repo.js +205 -0
- package/src/scan-url.js +563 -0
- package/src/utils/auth.js +13 -0
- package/src/utils/http.js +38 -0
- package/src/utils/url.js +54 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
# Remediation playbook
|
|
2
|
+
|
|
3
|
+
Step-by-step, framework-aware fixes for the issues Penthera finds. This is the
|
|
4
|
+
"fix" half of the scan -> fix -> verify loop in Workflow 4 of [SKILL.md](../SKILL.md).
|
|
5
|
+
|
|
6
|
+
## How to use this playbook
|
|
7
|
+
|
|
8
|
+
1. **Scan** and read the findings (each has a `title`, `category`, and `severity`).
|
|
9
|
+
2. **Detect the framework**: check `package.json`/imports for `next`, `express`,
|
|
10
|
+
`fastify`, `hono`; check `requirements.txt`/imports for `fastapi`, `flask`.
|
|
11
|
+
3. **For each finding**, find the section below that matches its `category`, then
|
|
12
|
+
apply the snippet for the detected framework as a reviewable diff.
|
|
13
|
+
4. **Re-scan** and confirm the finding is gone.
|
|
14
|
+
|
|
15
|
+
Rules: work highest severity first, **show the diff and get the user's approval
|
|
16
|
+
before applying**, never apply against production, and treat a fix as done only
|
|
17
|
+
after a clean re-scan.
|
|
18
|
+
|
|
19
|
+
| Finding category | Section |
|
|
20
|
+
|------------------|---------|
|
|
21
|
+
| `headers` | [Security headers](#security-headers) |
|
|
22
|
+
| `auth`, `auth-bypass` | [Authentication](#authentication) |
|
|
23
|
+
| `exposure` | [Exposed API docs](#exposed-api-docs) |
|
|
24
|
+
| `cors`, `cors-misconfiguration` | [CORS](#cors) |
|
|
25
|
+
| `rate-limiting` | [Rate limiting](#rate-limiting) |
|
|
26
|
+
| `secrets` | [Hardcoded secrets](#hardcoded-secrets) |
|
|
27
|
+
| `auth-bypass` (JWT) | [JWT](#jwt) |
|
|
28
|
+
| `open-redirect` | [Open redirect](#open-redirect) |
|
|
29
|
+
| `sqli`/`xss`/`ssti`/`ssrf`/`cmdi`/`injection` | [Injection](#injection) |
|
|
30
|
+
| `transport`, `tls` | [HTTPS and TLS](#https-and-tls) |
|
|
31
|
+
| `cookie` | [Insecure cookies](#insecure-cookies) |
|
|
32
|
+
| `disclosure`, `info-leak` | [Information disclosure](#information-disclosure) |
|
|
33
|
+
| `js-vulnerability`, `cve` | [Vulnerable JS libraries](#vulnerable-js-libraries) |
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Security headers
|
|
38
|
+
|
|
39
|
+
**Findings:** Missing Content-Security-Policy, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, Permissions-Policy, HSTS.
|
|
40
|
+
**Means:** responses lack headers that mitigate clickjacking, MIME sniffing, and content injection.
|
|
41
|
+
|
|
42
|
+
**Next.js** (`next.config.js`):
|
|
43
|
+
```js
|
|
44
|
+
const securityHeaders = [
|
|
45
|
+
{ key: "X-Frame-Options", value: "DENY" },
|
|
46
|
+
{ key: "X-Content-Type-Options", value: "nosniff" },
|
|
47
|
+
{ key: "Referrer-Policy", value: "strict-origin-when-cross-origin" },
|
|
48
|
+
{ key: "Permissions-Policy", value: "camera=(), microphone=(), geolocation=()" },
|
|
49
|
+
{ key: "Content-Security-Policy", value: "default-src 'self'" },
|
|
50
|
+
{ key: "Strict-Transport-Security", value: "max-age=63072000; includeSubDomains; preload" },
|
|
51
|
+
];
|
|
52
|
+
module.exports = {
|
|
53
|
+
async headers() {
|
|
54
|
+
return [{ source: "/:path*", headers: securityHeaders }];
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
**Express** ([helmet](https://www.npmjs.com/package/helmet)):
|
|
60
|
+
```js
|
|
61
|
+
import helmet from "helmet";
|
|
62
|
+
app.use(helmet()); // CSP, X-Frame-Options, HSTS, X-Content-Type-Options, Referrer-Policy
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
**Fastify:** `await app.register(import("@fastify/helmet"))`
|
|
66
|
+
**Hono:** `import { secureHeaders } from "hono/secure-headers"; app.use("*", secureHeaders())`
|
|
67
|
+
|
|
68
|
+
**FastAPI** (middleware):
|
|
69
|
+
```python
|
|
70
|
+
@app.middleware("http")
|
|
71
|
+
async def security_headers(request, call_next):
|
|
72
|
+
resp = await call_next(request)
|
|
73
|
+
resp.headers["X-Frame-Options"] = "DENY"
|
|
74
|
+
resp.headers["X-Content-Type-Options"] = "nosniff"
|
|
75
|
+
resp.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
|
|
76
|
+
resp.headers["Content-Security-Policy"] = "default-src 'self'"
|
|
77
|
+
return resp
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
**Verify:** re-scan; the "Missing ... header" findings disappear.
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## Authentication
|
|
85
|
+
|
|
86
|
+
**Findings:** Unauthenticated endpoint, Client-side-only authentication, Login accepts arbitrary credentials.
|
|
87
|
+
**Means:** sensitive routes are reachable without server-side auth, or auth is only enforced in the browser (which is not security).
|
|
88
|
+
|
|
89
|
+
**Next.js** (`middleware.ts` plus per-route verification):
|
|
90
|
+
```ts
|
|
91
|
+
import { NextResponse } from "next/server";
|
|
92
|
+
export function middleware(req) {
|
|
93
|
+
const token = req.cookies.get("session")?.value;
|
|
94
|
+
if (!token) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
|
95
|
+
}
|
|
96
|
+
export const config = { matcher: ["/api/:path*"] };
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Express:**
|
|
100
|
+
```js
|
|
101
|
+
function requireAuth(req, res, next) {
|
|
102
|
+
const token = req.headers.authorization?.replace("Bearer ", "");
|
|
103
|
+
if (!token || !verify(token)) return res.status(401).json({ error: "Unauthorized" });
|
|
104
|
+
next();
|
|
105
|
+
}
|
|
106
|
+
app.use("/api/admin", requireAuth);
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**FastAPI:**
|
|
110
|
+
```python
|
|
111
|
+
from fastapi import Depends, HTTPException
|
|
112
|
+
def require_auth(token: str = Depends(oauth2_scheme)):
|
|
113
|
+
if not valid(token): raise HTTPException(status_code=401)
|
|
114
|
+
|
|
115
|
+
@app.get("/api/admin", dependencies=[Depends(require_auth)])
|
|
116
|
+
def admin(): ...
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
**Verify:** re-scan; the route returns 401 without a valid token and the "Unauthenticated endpoint" finding clears.
|
|
120
|
+
|
|
121
|
+
---
|
|
122
|
+
|
|
123
|
+
## Exposed API docs
|
|
124
|
+
|
|
125
|
+
**Findings:** FastAPI/Swagger UI exposed, OpenAPI specification exposed.
|
|
126
|
+
**Fix:** disable interactive docs and the spec in production.
|
|
127
|
+
|
|
128
|
+
**FastAPI:** `FastAPI(docs_url=None, redoc_url=None, openapi_url=None)` in production (gate by an env flag).
|
|
129
|
+
**Express + swagger-ui:** only mount when `process.env.NODE_ENV !== "production"`.
|
|
130
|
+
|
|
131
|
+
**Verify:** `/docs` and `/openapi.json` return 404 in production.
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## CORS
|
|
136
|
+
|
|
137
|
+
**Finding:** the server reflects an arbitrary `Origin`, or uses `*` together with credentials.
|
|
138
|
+
**Fix:** allowlist explicit origins; never reflect a user-supplied origin with credentials.
|
|
139
|
+
|
|
140
|
+
**Express** ([cors](https://www.npmjs.com/package/cors)):
|
|
141
|
+
```js
|
|
142
|
+
import cors from "cors";
|
|
143
|
+
app.use(cors({ origin: ["https://app.example.com"], credentials: true }));
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**FastAPI:**
|
|
147
|
+
```python
|
|
148
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
149
|
+
app.add_middleware(CORSMiddleware, allow_origins=["https://app.example.com"], allow_credentials=True)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**Next.js / Hono:** set `Access-Control-Allow-Origin` to a fixed allowlist value, not the request `Origin`.
|
|
153
|
+
|
|
154
|
+
**Verify:** re-scan; the CORS finding clears once only allowlisted origins are reflected.
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Rate limiting
|
|
159
|
+
|
|
160
|
+
**Finding:** No login rate limiting detected.
|
|
161
|
+
**Fix:** throttle auth and other expensive endpoints.
|
|
162
|
+
|
|
163
|
+
**Express** ([express-rate-limit](https://www.npmjs.com/package/express-rate-limit)):
|
|
164
|
+
```js
|
|
165
|
+
import rateLimit from "express-rate-limit";
|
|
166
|
+
app.use("/api/login", rateLimit({ windowMs: 15 * 60 * 1000, max: 10 }));
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**Fastify:** `@fastify/rate-limit`. **Next.js:** an edge limiter such as `@upstash/ratelimit` in `middleware.ts`.
|
|
170
|
+
**FastAPI:** [slowapi](https://pypi.org/project/slowapi/) with `@limiter.limit("10/15minutes")`.
|
|
171
|
+
|
|
172
|
+
**Verify:** re-scan; rapid repeated requests are throttled (HTTP 429).
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Hardcoded secrets
|
|
177
|
+
|
|
178
|
+
**Finding:** an API key, token, password, or private key is committed in the repo.
|
|
179
|
+
**Fix (do all three):**
|
|
180
|
+
1. Move it to an environment variable: `const key = process.env.STRIPE_SECRET_KEY;` (was `const key = "sk_live_..."`).
|
|
181
|
+
2. **Rotate the secret now**, once committed it is compromised, even after removal.
|
|
182
|
+
3. Add the file to `.gitignore`; if it was pushed, scrub history (`git filter-repo`).
|
|
183
|
+
|
|
184
|
+
**Verify:** re-scan the repo (`penthera --repo .`); the secret finding clears. Confirm the old secret is revoked.
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
## JWT
|
|
189
|
+
|
|
190
|
+
**Findings:** JWT alg:none bypass accepted, Configured JWT is expired.
|
|
191
|
+
**Fix:** pin the signing algorithm and verify expiry on every request.
|
|
192
|
+
```js
|
|
193
|
+
jwt.verify(token, publicKey, { algorithms: ["RS256"] }); // never accept "none"
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
**Verify:** re-scan; the alg:none and expiry findings clear.
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
## Open redirect
|
|
201
|
+
|
|
202
|
+
**Finding:** OAuth open redirect, `redirect_uri` (or a `return`/`next` param) reflects an arbitrary URL.
|
|
203
|
+
**Fix:** validate against an allowlist of exact registered URIs; never redirect to a raw user-supplied URL.
|
|
204
|
+
```js
|
|
205
|
+
const ALLOWED = new Set(["https://app.example.com/callback"]);
|
|
206
|
+
if (!ALLOWED.has(redirectUri)) return res.status(400).send("Invalid redirect_uri");
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
**Verify:** re-scan; the open-redirect finding clears.
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## Injection
|
|
214
|
+
|
|
215
|
+
**Findings:** SQL error leaked, OS command injection, XSS, SSTI, SSRF.
|
|
216
|
+
|
|
217
|
+
- **SQLi:** use parameterized queries; never concatenate input. `db.query("SELECT * FROM users WHERE id = $1", [id])`.
|
|
218
|
+
- **XSS:** rely on framework auto-escaping; set a CSP; avoid `dangerouslySetInnerHTML`/`v-html`/`|safe` with user input.
|
|
219
|
+
- **Command injection:** never pass input to a shell. Use `execFile(cmd, [args])` with an allowlist; avoid `exec`/`shell=True`.
|
|
220
|
+
- **SSRF:** allowlist outbound hosts; block internal ranges and metadata endpoints (`169.254.169.254`, `127.0.0.0/8`, `10.0.0.0/8`).
|
|
221
|
+
- **SSTI:** never render user input as a template; use sandboxed templating with autoescaping.
|
|
222
|
+
|
|
223
|
+
**Verify:** re-scan with `--deep`; the injection probe no longer triggers.
|
|
224
|
+
|
|
225
|
+
---
|
|
226
|
+
|
|
227
|
+
## HTTPS and TLS
|
|
228
|
+
|
|
229
|
+
**Findings:** No HTTPS (cleartext), TLS certificate expired / expiring / not trusted.
|
|
230
|
+
**Fix:** serve over HTTPS, redirect HTTP -> HTTPS, add HSTS, and renew/replace the certificate (e.g. Let's Encrypt). For "not trusted", fix the certificate chain (include intermediates).
|
|
231
|
+
|
|
232
|
+
**Verify:** re-scan; the transport/TLS findings clear.
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## Insecure cookies
|
|
237
|
+
|
|
238
|
+
**Finding:** a session cookie is missing `Secure`, `HttpOnly`, or `SameSite`.
|
|
239
|
+
**Fix:**
|
|
240
|
+
```js
|
|
241
|
+
res.cookie("session", token, { httpOnly: true, secure: true, sameSite: "lax" });
|
|
242
|
+
```
|
|
243
|
+
**Next.js:** `cookies().set("session", token, { httpOnly: true, secure: true, sameSite: "lax" })`.
|
|
244
|
+
|
|
245
|
+
**Verify:** re-scan; the cookie finding clears.
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## Information disclosure
|
|
250
|
+
|
|
251
|
+
**Findings:** Server version disclosed, stack traces or framework errors leaked.
|
|
252
|
+
**Fix:** remove the `Server`/`X-Powered-By` headers (`app.disable("x-powered-by")` in Express) and return generic errors in production (no stack traces, no raw SQL errors).
|
|
253
|
+
|
|
254
|
+
**Verify:** re-scan; the disclosure finding clears.
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## Vulnerable JS libraries
|
|
259
|
+
|
|
260
|
+
**Finding:** a front-end JavaScript library has known CVEs (detected via Retire.js).
|
|
261
|
+
**Fix:** upgrade the library to a patched version (bump it in `package.json`, `npm update <pkg>`), or replace it. Re-run `npm audit`.
|
|
262
|
+
|
|
263
|
+
**Verify:** re-scan; once the patched version is served, the JS CVE finding clears.
|
|
264
|
+
|
|
265
|
+
---
|
|
266
|
+
|
|
267
|
+
For building secure-by-default so these never appear, see [secure-defaults.md](secure-defaults.md).
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Owner-facing reports & questionnaire responses
|
|
2
|
+
|
|
3
|
+
Turn a Penthera scan into documents a non-security owner can act on and send:
|
|
4
|
+
a **remediation plan**, a **security-questionnaire response**, and a **NIS2
|
|
5
|
+
duty-of-care evidence summary**. All three are generated by you (the agent) from
|
|
6
|
+
the scan's JSON, grounded in what the scan actually found and tested.
|
|
7
|
+
|
|
8
|
+
## Golden rule: evidence, not assertions
|
|
9
|
+
|
|
10
|
+
You are drafting from technical evidence, not certifying compliance.
|
|
11
|
+
|
|
12
|
+
- **Only claim what the scan supports.** If a finding or `executedProbes` entry
|
|
13
|
+
backs an answer, cite it. If it doesn't, mark the answer **`NEEDS HUMAN INPUT`**
|
|
14
|
+
, never invent a control, a policy, or a "yes".
|
|
15
|
+
- **Organisational questions are not answerable from a scan.** Incident-response
|
|
16
|
+
process, staff training, backups, vendor contracts, data-retention policy →
|
|
17
|
+
always `NEEDS HUMAN INPUT`. Say so plainly.
|
|
18
|
+
- **Never state or imply certification.** Penthera produces evidence toward a
|
|
19
|
+
duty of care; it does not certify anything. Keep the disclaimer in every doc.
|
|
20
|
+
- Prefer "as of <scan date>, an automated scan found…" phrasing over absolutes.
|
|
21
|
+
|
|
22
|
+
## The data you have
|
|
23
|
+
|
|
24
|
+
Run a scan with JSON + framework mapping, then read the report object:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
penthera <url> --repo . --framework nis2 -o reports/scan.json
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Key fields in `reports/scan.json`:
|
|
31
|
+
|
|
32
|
+
| Field | Use |
|
|
33
|
+
|-------|-----|
|
|
34
|
+
| `findings[]` | `{severity, title, description, url, category, source, wstg[]}`, the raw evidence |
|
|
35
|
+
| `actionPlan[]` | prioritised `{action, why, fix, effort, ownerAction, severity, findings[]}`, the owner remediation plan, already built |
|
|
36
|
+
| `compliance` | NIS2 Article 21 per-measure status (`--framework nis2`), tested / evidence / planned / out-of-scope |
|
|
37
|
+
| `executedProbes[]` | which probes actually ran, the basis for "we tested X" |
|
|
38
|
+
| `emailAuth` | `{spf, dmarc, dmarcPolicy, mxCount}`, email posture |
|
|
39
|
+
| `tls`, `fingerprint`, `endpoints` | transport, stack, and surface facts |
|
|
40
|
+
|
|
41
|
+
## Document 1, Remediation plan (for the owner)
|
|
42
|
+
|
|
43
|
+
The scan's Markdown report already contains an **Action plan** section built from
|
|
44
|
+
`actionPlan[]`. To go further, expand each item into a ticket the owner can hand
|
|
45
|
+
to a developer:
|
|
46
|
+
|
|
47
|
+
For each `actionPlan[]` item, in order:
|
|
48
|
+
1. **What & why**, from `action` + `why`.
|
|
49
|
+
2. **How**, from `fix`, made specific to their stack (detect Next.js/Express/
|
|
50
|
+
Fastify/FastAPI from `package.json`/imports and give the exact config).
|
|
51
|
+
3. **Owner vs developer**, if `ownerAction` is true, call it out (rotate secret,
|
|
52
|
+
renew cert, change DNS): the owner must do it; a developer/agent cannot.
|
|
53
|
+
4. **Verify**, "re-scan with `--baseline` to confirm this finding is gone."
|
|
54
|
+
|
|
55
|
+
Offer to apply the developer-side fixes via **Workflow 4** (scan → fix → verify).
|
|
56
|
+
|
|
57
|
+
## Document 2, Security-questionnaire response
|
|
58
|
+
|
|
59
|
+
Vendors get sent questionnaires (SIG Lite, CAIQ, or a customer's own list). Draft
|
|
60
|
+
a response grounded in the scan. Use this table format and fill the **Evidence**
|
|
61
|
+
column from scan data; mark anything unbacked `NEEDS HUMAN INPUT`.
|
|
62
|
+
|
|
63
|
+
```markdown
|
|
64
|
+
# Security questionnaire, <company>, as of <scan date>
|
|
65
|
+
|
|
66
|
+
> Draft prepared from an automated Penthera scan of <target>. Answers marked
|
|
67
|
+
> NEEDS HUMAN INPUT require confirmation by the owner. This is not a certification.
|
|
68
|
+
|
|
69
|
+
| # | Question | Answer | Evidence |
|
|
70
|
+
|---|----------|--------|----------|
|
|
71
|
+
| 1 | Is all traffic served over HTTPS/TLS? | Yes/No | `tls.protocol`, HSTS in `fingerprint` |
|
|
72
|
+
| 2 | Are security headers (CSP, X-Frame-Options…) set? | Partial | header findings / absence |
|
|
73
|
+
| 3 | Is email spoofing protection (SPF/DMARC) in place? | Yes/Partial/No | `emailAuth` |
|
|
74
|
+
| 4 | Are dependencies scanned for known CVEs? | Yes | Retire.js probe ran (`executedProbes`) + findings |
|
|
75
|
+
| 5 | Is access control tested (authz, IDOR)? | Yes | auth/idor probes in `executedProbes` |
|
|
76
|
+
| 6 | Are secrets kept out of source control? | Yes/No | secret-scan findings |
|
|
77
|
+
| 7 | Do you have an incident-response process? | NEEDS HUMAN INPUT | organisational |
|
|
78
|
+
| 8 | Do you run regular backups / DR tests? | NEEDS HUMAN INPUT | organisational |
|
|
79
|
+
| 9 | Is staff security-trained? | NEEDS HUMAN INPUT | organisational |
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Map each questionnaire item to evidence where it exists; be conservative. A
|
|
83
|
+
"Yes" needs a backing probe/finding; otherwise "Partial" (with the gap) or
|
|
84
|
+
`NEEDS HUMAN INPUT`.
|
|
85
|
+
|
|
86
|
+
## Document 3, NIS2 duty-of-care evidence summary
|
|
87
|
+
|
|
88
|
+
From `compliance` (`--framework nis2`). One row per Article 21(2) measure with
|
|
89
|
+
its per-scan status and the evidence, plus the standard disclaimer.
|
|
90
|
+
|
|
91
|
+
```markdown
|
|
92
|
+
# NIS2 Article 21, technical evidence summary (<scan date>)
|
|
93
|
+
|
|
94
|
+
> Automated technical evidence toward the duty of care (zorgplicht), not a
|
|
95
|
+
> compliance determination, an audit, or a certification. Organisational
|
|
96
|
+
> measures need human process; consult your national authority (NCSC in NL, CCB
|
|
97
|
+
> in BE) or a qualified jurist.
|
|
98
|
+
|
|
99
|
+
| Art. 21(2) | Measure | Status this scan | Evidence / gap |
|
|
100
|
+
|-----------|---------|------------------|----------------|
|
|
101
|
+
| (from compliance.measures: id, title, status, findings, note) |
|
|
102
|
+
|
|
103
|
+
**Testable measures exercised this scan:** <compliance.summary.coveredThisScan>/<compliance.summary.testable>.
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Fill each row from `compliance.measures[]`. For `planned` / `out-of-scope` /
|
|
107
|
+
`evidence` measures, say so honestly rather than implying coverage.
|
|
108
|
+
|
|
109
|
+
## Output
|
|
110
|
+
|
|
111
|
+
Write drafts to `reports/` (gitignored) as Markdown. Offer to convert to DOCX/PDF
|
|
112
|
+
if the user has a converter. Always end by listing every `NEEDS HUMAN INPUT` item
|
|
113
|
+
so the owner knows exactly what only they can answer.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Secure defaults
|
|
2
|
+
|
|
3
|
+
Build it right the first time so Penthera finds nothing. This is the proactive
|
|
4
|
+
companion to [remediation.md](remediation.md): apply these while writing code,
|
|
5
|
+
then run a scan to verify. For fixing issues that already exist, use the
|
|
6
|
+
remediation playbook.
|
|
7
|
+
|
|
8
|
+
## Checklist for every web app
|
|
9
|
+
|
|
10
|
+
- [ ] **Security headers** set (CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy, HSTS).
|
|
11
|
+
- [ ] **Server-side auth** enforced on every sensitive route. Client-side checks are not security.
|
|
12
|
+
- [ ] **Parameterized queries** everywhere. No string-built SQL.
|
|
13
|
+
- [ ] **Output encoding** on by default. No raw HTML injection with user input.
|
|
14
|
+
- [ ] **Secrets in environment variables**, never in code. `.env` is gitignored.
|
|
15
|
+
- [ ] **CORS allowlist** of explicit origins. No `*` with credentials.
|
|
16
|
+
- [ ] **Rate limiting** on auth and expensive endpoints.
|
|
17
|
+
- [ ] **Secure cookies**: `HttpOnly`, `Secure`, `SameSite`.
|
|
18
|
+
- [ ] **API docs disabled** in production (`/docs`, `/openapi.json`).
|
|
19
|
+
- [ ] **HTTPS only** with HSTS and a valid TLS certificate.
|
|
20
|
+
- [ ] **JWT** with a pinned algorithm (no `alg:none`); verify `exp` and signature.
|
|
21
|
+
- [ ] **Redirect targets** validated against an allowlist.
|
|
22
|
+
|
|
23
|
+
## Per-framework starting points
|
|
24
|
+
|
|
25
|
+
| Framework | Secure-by-default setup |
|
|
26
|
+
|-----------|--------------------------|
|
|
27
|
+
| **Next.js** | Security headers in `next.config.js`; auth in `middleware.ts`; cookies via `next/headers` with `httpOnly`/`secure`/`sameSite`. |
|
|
28
|
+
| **Express** | `helmet()` + `cors({ origin: [...] })` + `express-rate-limit` + an auth middleware; `app.disable("x-powered-by")`. |
|
|
29
|
+
| **Fastify** | `@fastify/helmet` + `@fastify/cors` + `@fastify/rate-limit` + an `onRequest` auth hook. |
|
|
30
|
+
| **Hono** | `secureHeaders()` + `cors()` + custom auth middleware. |
|
|
31
|
+
| **FastAPI** | header middleware + `CORSMiddleware` allowlist + `slowapi` + `docs_url=None` in production. |
|
|
32
|
+
|
|
33
|
+
## Verify
|
|
34
|
+
|
|
35
|
+
Run a scan to confirm the defaults hold:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
penthera https://localhost:3000 --repo .
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Anything it flags, fix with the [remediation playbook](remediation.md), then re-scan.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Troubleshooting
|
|
2
|
+
|
|
3
|
+
## Exit code 2, scan failed
|
|
4
|
+
|
|
5
|
+
**Symptoms:** CLI exits with code 2, "Target unreachable" or "Scan failed"
|
|
6
|
+
|
|
7
|
+
**Causes and fixes:**
|
|
8
|
+
|
|
9
|
+
| Error | Cause | Solution |
|
|
10
|
+
|-------|-------|----------|
|
|
11
|
+
| Target unreachable | Server down, wrong URL, firewall | Verify URL in browser; check server is running |
|
|
12
|
+
| No target specified | Missing URL and `--repo` | Provide URL, `--repo <path>`, or `--machine` |
|
|
13
|
+
| Unknown profile | Invalid `--profile` value | Use `quick`, `standard`, or `deep` |
|
|
14
|
+
| Scan failed (exception) | Network timeout, parse error | Retry with `--verbose`; increase `--timeout` |
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
penthera https://myapp.com --verbose --timeout 30000
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Node.js not found or wrong version
|
|
21
|
+
|
|
22
|
+
**Symptoms:** `node: command not found` or engine error
|
|
23
|
+
|
|
24
|
+
**Solution:** Install Node.js 18+ from [nodejs.org](https://nodejs.org/). Verify:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
node -v # should be v18.x or higher
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Penthera CLI not found
|
|
31
|
+
|
|
32
|
+
**Symptoms:** `penthera: command not found`
|
|
33
|
+
|
|
34
|
+
**Solutions:**
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
# From repo root (always works)
|
|
38
|
+
node bin/penthera.js --version
|
|
39
|
+
|
|
40
|
+
# Global install
|
|
41
|
+
cd /path/to/penthera && npm link
|
|
42
|
+
penthera --version
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Auth failures / empty authenticated findings
|
|
46
|
+
|
|
47
|
+
**Symptoms:** Scan completes but misses protected endpoints
|
|
48
|
+
|
|
49
|
+
**Causes:** Expired token, wrong cookie format, missing Bearer prefix
|
|
50
|
+
|
|
51
|
+
**Solutions:**
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# Bearer, prefix added automatically if missing
|
|
55
|
+
penthera https://myapp.com --auth-bearer "eyJ..."
|
|
56
|
+
|
|
57
|
+
# Or via env
|
|
58
|
+
export PENTHERA_BEARER="eyJ..."
|
|
59
|
+
penthera https://myapp.com -o reports/scan.json
|
|
60
|
+
|
|
61
|
+
# Cookie, full header value
|
|
62
|
+
penthera https://myapp.com --auth-cookie "session=abc123; path=/"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Deep/fuzz warnings
|
|
66
|
+
|
|
67
|
+
**Symptoms:** Yellow WARN about attack payloads
|
|
68
|
+
|
|
69
|
+
**Expected:** `--deep` always prints a warning. Confirm user authorization before proceeding.
|
|
70
|
+
|
|
71
|
+
## Baseline file not found
|
|
72
|
+
|
|
73
|
+
**Symptoms:** Error reading baseline JSON
|
|
74
|
+
|
|
75
|
+
**Solution:** Ensure previous scan exists:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
ls reports/previous.json
|
|
79
|
+
# Or run initial scan first:
|
|
80
|
+
penthera https://myapp.com -o reports/previous.json
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## SARIF upload fails in CI
|
|
84
|
+
|
|
85
|
+
**Symptoms:** GitHub Actions SARIF upload error
|
|
86
|
+
|
|
87
|
+
**Checks:**
|
|
88
|
+
- `reports/scan.sarif` was generated (`--sarif` flag used)
|
|
89
|
+
- `github/codeql-action/upload-sarif@v3` step has `if: always()`
|
|
90
|
+
- Repository has code scanning enabled
|
|
91
|
+
|
|
92
|
+
## Preflight script failures
|
|
93
|
+
|
|
94
|
+
Run manually for diagnostics:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
bash skills/penthera/scripts/preflight.sh https://myapp.com
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Fix each reported check before scanning.
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Penthera preflight, run before first scan in a session.
|
|
3
|
+
# Usage: bash skills/penthera/scripts/preflight.sh [URL]
|
|
4
|
+
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
|
|
7
|
+
TARGET_URL="${1:-}"
|
|
8
|
+
REPO_ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
|
|
9
|
+
|
|
10
|
+
fail() {
|
|
11
|
+
echo "preflight: ERROR: $*" >&2
|
|
12
|
+
exit 1
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
warn() {
|
|
16
|
+
echo "preflight: WARN: $*" >&2
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
ok() {
|
|
20
|
+
echo "preflight: OK: $*"
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
# Node.js 18+
|
|
24
|
+
if ! command -v node >/dev/null 2>&1; then
|
|
25
|
+
fail "Node.js not found. Install Node.js 18+ from https://nodejs.org/"
|
|
26
|
+
fi
|
|
27
|
+
|
|
28
|
+
NODE_MAJOR=$(node -e "process.stdout.write(String(process.version).replace(/^v/, '').split('.')[0])")
|
|
29
|
+
if [ "$NODE_MAJOR" -lt 18 ]; then
|
|
30
|
+
fail "Node.js $(node -v) is too old. Requires Node.js 18+."
|
|
31
|
+
fi
|
|
32
|
+
ok "Node.js $(node -v)"
|
|
33
|
+
|
|
34
|
+
# Penthera CLI
|
|
35
|
+
PENTHERA_CMD=""
|
|
36
|
+
if command -v penthera >/dev/null 2>&1; then
|
|
37
|
+
PENTHERA_CMD="penthera"
|
|
38
|
+
elif [ -f "$REPO_ROOT/bin/penthera.js" ]; then
|
|
39
|
+
PENTHERA_CMD="node $REPO_ROOT/bin/penthera.js"
|
|
40
|
+
else
|
|
41
|
+
fail "Penthera CLI not found. Run 'npm link' from repo root or use node bin/penthera.js"
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
VERSION=$($PENTHERA_CMD --version 2>/dev/null || true)
|
|
45
|
+
if [ -z "$VERSION" ]; then
|
|
46
|
+
fail "Could not run Penthera CLI ($PENTHERA_CMD --version failed)"
|
|
47
|
+
fi
|
|
48
|
+
ok "Penthera $VERSION"
|
|
49
|
+
|
|
50
|
+
# Reports directory (create if missing)
|
|
51
|
+
mkdir -p "$REPO_ROOT/reports"
|
|
52
|
+
ok "reports/ directory ready"
|
|
53
|
+
|
|
54
|
+
# Non-localhost warning
|
|
55
|
+
if [ -n "$TARGET_URL" ]; then
|
|
56
|
+
case "$TARGET_URL" in
|
|
57
|
+
http://localhost*|https://localhost*|http://127.0.0.1*|https://127.0.0.1*|http://[::1]*|https://[::1]*)
|
|
58
|
+
ok "Target is localhost, authorization satisfied for local testing"
|
|
59
|
+
;;
|
|
60
|
+
*)
|
|
61
|
+
warn "Target is not localhost ($TARGET_URL). Confirm user owns this system or has written authorization before scanning."
|
|
62
|
+
;;
|
|
63
|
+
esac
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
echo "preflight: All checks passed. Ready to scan."
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Validate a Penthera JSON scan report.
|
|
4
|
+
* Usage: node skills/penthera/scripts/validate-report.mjs [report.json]
|
|
5
|
+
*/
|
|
6
|
+
import { readFileSync, existsSync } from "node:fs";
|
|
7
|
+
import { resolve } from "node:path";
|
|
8
|
+
|
|
9
|
+
const reportPath = resolve(process.argv[2] || "reports/scan.json");
|
|
10
|
+
|
|
11
|
+
function fail(msg) {
|
|
12
|
+
console.error(`validate-report: ERROR: ${msg}`);
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function ok(msg) {
|
|
17
|
+
console.log(`validate-report: OK: ${msg}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!existsSync(reportPath)) {
|
|
21
|
+
fail(`Report not found: ${reportPath}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let report;
|
|
25
|
+
try {
|
|
26
|
+
report = JSON.parse(readFileSync(reportPath, "utf-8"));
|
|
27
|
+
} catch (e) {
|
|
28
|
+
fail(`Invalid JSON: ${e.message}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (!report.timestamp || typeof report.timestamp !== "string") {
|
|
32
|
+
fail("Missing or invalid 'timestamp' field");
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (!Array.isArray(report.findings)) {
|
|
36
|
+
fail("Missing or invalid 'findings' array");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const requiredFindingFields = ["severity", "title"];
|
|
40
|
+
for (const [i, f] of report.findings.entries()) {
|
|
41
|
+
for (const field of requiredFindingFields) {
|
|
42
|
+
if (!f[field]) fail(`Finding #${i} missing '${field}'`);
|
|
43
|
+
}
|
|
44
|
+
const validSev = ["critical", "high", "medium", "low", "info"];
|
|
45
|
+
if (!validSev.includes(f.severity)) {
|
|
46
|
+
fail(`Finding #${i} has invalid severity: ${f.severity}`);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const mdPath = reportPath.replace(/\.json$/i, ".md");
|
|
51
|
+
if (existsSync(mdPath)) ok(`Companion Markdown found: ${mdPath}`);
|
|
52
|
+
|
|
53
|
+
ok(`${report.findings.length} findings in ${reportPath}`);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Validate a Penthera JSON scan report.
|
|
3
|
+
# Usage: bash skills/penthera/scripts/validate-report.sh [report.json]
|
|
4
|
+
|
|
5
|
+
set -euo pipefail
|
|
6
|
+
REPO_ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
|
|
7
|
+
REPORT="${1:-reports/scan.json}"
|
|
8
|
+
node "$REPO_ROOT/skills/penthera/scripts/validate-report.mjs" "$REPORT"
|