@hoanghainh/komagu 2.0.0 → 2.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 +20 -0
- package/README.md +1 -1
- package/assess/lsa/examples/eval-run-4/ground-truth.json +823 -0
- package/assess/lsa/examples/eval-run-4/iter0-gptoss-a/ai-run-stats.json +58 -0
- package/assess/lsa/examples/eval-run-4/iter0-gptoss-a/coverage.json +85 -0
- package/assess/lsa/examples/eval-run-4/iter0-gptoss-a/findings/priv-privilege-001.json +822 -0
- package/assess/lsa/examples/eval-run-4/iter0-gptoss-a/findings/priv-privilege-002.json +393 -0
- package/assess/lsa/examples/eval-run-4/iter0-gptoss-a/findings.json +4477 -0
- package/assess/lsa/examples/eval-run-4/iter0-gptoss-a/mass-assignment.candidates.json +232 -0
- package/assess/lsa/examples/eval-run-4/iter0-gptoss-a/routes/routes-001.json +425 -0
- package/assess/lsa/examples/eval-run-4/iter0-gptoss-a/routes/routes-002.json +333 -0
- package/assess/lsa/examples/eval-run-4/iter0-gptoss-a/routes/routes-003.json +400 -0
- package/assess/lsa/examples/eval-run-4/iter0-gptoss-a/routes/routes-004.json +403 -0
- package/assess/lsa/examples/eval-run-4/iter0-gptoss-a/routes/routes-005.json +398 -0
- package/assess/lsa/examples/eval-run-4/iter0-gptoss-a/routes/routes-006.json +63 -0
- package/assess/lsa/examples/eval-run-4/iter0-gptoss-a/score.json +120 -0
- package/assess/lsa/examples/eval-run-4/iter1-gptoss-a/ai-run-stats.json +55 -0
- package/assess/lsa/examples/eval-run-4/iter1-gptoss-a/coverage.json +50 -0
- package/assess/lsa/examples/eval-run-4/iter1-gptoss-a/findings/priv-privilege-001.json +613 -0
- package/assess/lsa/examples/eval-run-4/iter1-gptoss-a/findings/priv-privilege-002.json +287 -0
- package/assess/lsa/examples/eval-run-4/iter1-gptoss-a/findings.json +4870 -0
- package/assess/lsa/examples/eval-run-4/iter1-gptoss-a/mass-assignment.candidates.json +232 -0
- package/assess/lsa/examples/eval-run-4/iter1-gptoss-a/routes/routes-001.json +425 -0
- package/assess/lsa/examples/eval-run-4/iter1-gptoss-a/routes/routes-002.json +333 -0
- package/assess/lsa/examples/eval-run-4/iter1-gptoss-a/routes/routes-003.json +400 -0
- package/assess/lsa/examples/eval-run-4/iter1-gptoss-a/routes/routes-004.json +403 -0
- package/assess/lsa/examples/eval-run-4/iter1-gptoss-a/routes/routes-005.json +398 -0
- package/assess/lsa/examples/eval-run-4/iter1-gptoss-a/routes/routes-006.json +63 -0
- package/assess/lsa/examples/eval-run-4/iter1-gptoss-a/score.json +107 -0
- package/assess/lsa/examples/eval-run-4/iter1-gptoss-b/ai-run-stats.json +58 -0
- package/assess/lsa/examples/eval-run-4/iter1-gptoss-b/coverage.json +92 -0
- package/assess/lsa/examples/eval-run-4/iter1-gptoss-b/findings/priv-privilege-001.json +645 -0
- package/assess/lsa/examples/eval-run-4/iter1-gptoss-b/findings/priv-privilege-002.json +222 -0
- package/assess/lsa/examples/eval-run-4/iter1-gptoss-b/findings.json +4859 -0
- package/assess/lsa/examples/eval-run-4/iter1-gptoss-b/mass-assignment.candidates.json +232 -0
- package/assess/lsa/examples/eval-run-4/iter1-gptoss-b/routes/routes-001.json +425 -0
- package/assess/lsa/examples/eval-run-4/iter1-gptoss-b/routes/routes-002.json +333 -0
- package/assess/lsa/examples/eval-run-4/iter1-gptoss-b/routes/routes-003.json +400 -0
- package/assess/lsa/examples/eval-run-4/iter1-gptoss-b/routes/routes-004.json +403 -0
- package/assess/lsa/examples/eval-run-4/iter1-gptoss-b/routes/routes-005.json +398 -0
- package/assess/lsa/examples/eval-run-4/iter1-gptoss-b/routes/routes-006.json +63 -0
- package/assess/lsa/examples/eval-run-4/iter1-gptoss-b/score.json +121 -0
- package/assess/lsa/examples/eval-run-4/iter1-qwen-a-incomplete/coverage.json +111 -0
- package/assess/lsa/examples/eval-run-4/iter1-qwen-a-incomplete/findings/priv-privilege-001.json +6 -0
- package/assess/lsa/examples/eval-run-4/iter1-qwen-a-incomplete/findings/priv-privilege-002.json +344 -0
- package/assess/lsa/examples/eval-run-4/iter1-qwen-a-incomplete/findings.json +3544 -0
- package/assess/lsa/examples/eval-run-4/iter1-qwen-a-incomplete/mass-assignment.candidates.json +232 -0
- package/assess/lsa/examples/eval-run-4/iter1-qwen-a-incomplete/routes/routes-001.json +425 -0
- package/assess/lsa/examples/eval-run-4/iter1-qwen-a-incomplete/routes/routes-002.json +333 -0
- package/assess/lsa/examples/eval-run-4/iter1-qwen-a-incomplete/routes/routes-003.json +400 -0
- package/assess/lsa/examples/eval-run-4/iter1-qwen-a-incomplete/routes/routes-004.json +403 -0
- package/assess/lsa/examples/eval-run-4/iter1-qwen-a-incomplete/routes/routes-005.json +398 -0
- package/assess/lsa/examples/eval-run-4/iter1-qwen-a-incomplete/routes/routes-006.json +63 -0
- package/assess/lsa/examples/eval-run-4/iter1-qwen-a-incomplete/score.json +123 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/ai-run-stats.json +50 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/coverage.json +42 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/findings/priv-privilege-001.json +540 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/findings/priv-privilege-002.json +238 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/findings.json +4383 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/findings.sarif +8729 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/mass-assignment.candidates.json +232 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/report.ja.md +1911 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/report.md +1911 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/routes/routes-001.json +425 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/routes/routes-002.json +333 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/routes/routes-003.json +400 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/routes/routes-004.json +403 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/routes/routes-005.json +398 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/routes/routes-006.json +63 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/score.json +101 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/stack-profile.json +226 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-b/ai-run-stats.json +54 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-b/coverage.json +66 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-b/findings/priv-privilege-001.json +440 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-b/findings/priv-privilege-002.json +229 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-b/findings.json +3424 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-b/mass-assignment.candidates.json +232 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-b/routes/routes-001.json +425 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-b/routes/routes-002.json +333 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-b/routes/routes-003.json +400 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-b/routes/routes-004.json +403 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-b/routes/routes-005.json +398 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-b/routes/routes-006.json +63 -0
- package/assess/lsa/examples/eval-run-4/iter2-gptoss-b/score.json +120 -0
- package/assess/lsa/examples/eval-run-5/gptoss-a/ai-run-stats.json +88 -0
- package/assess/lsa/examples/eval-run-5/gptoss-a/coverage.json +36 -0
- package/assess/lsa/examples/eval-run-5/gptoss-a/findings/priv-privilege-001.json +530 -0
- package/assess/lsa/examples/eval-run-5/gptoss-a/findings/priv-privilege-002.json +248 -0
- package/assess/lsa/examples/eval-run-5/gptoss-a/findings.json +4766 -0
- package/assess/lsa/examples/eval-run-5/gptoss-a/findings.sarif +9326 -0
- package/assess/lsa/examples/eval-run-5/gptoss-a/mass-assignment.candidates.json +232 -0
- package/assess/lsa/examples/eval-run-5/gptoss-a/report.ja.md +2011 -0
- package/assess/lsa/examples/eval-run-5/gptoss-a/report.md +2011 -0
- package/assess/lsa/examples/eval-run-5/gptoss-a/routes/routes-001.json +425 -0
- package/assess/lsa/examples/eval-run-5/gptoss-a/routes/routes-002.json +333 -0
- package/assess/lsa/examples/eval-run-5/gptoss-a/routes/routes-003.json +400 -0
- package/assess/lsa/examples/eval-run-5/gptoss-a/routes/routes-004.json +403 -0
- package/assess/lsa/examples/eval-run-5/gptoss-a/routes/routes-005.json +398 -0
- package/assess/lsa/examples/eval-run-5/gptoss-a/routes/routes-006.json +63 -0
- package/assess/lsa/examples/eval-run-5/gptoss-a/score.json +80 -0
- package/assess/lsa/examples/eval-run-5/gptoss-a/stack-profile.json +226 -0
- package/assess/lsa/examples/eval-run-5/gptoss-b/ai-run-stats.json +83 -0
- package/assess/lsa/examples/eval-run-5/gptoss-b/coverage.json +44 -0
- package/assess/lsa/examples/eval-run-5/gptoss-b/findings/priv-privilege-001.json +579 -0
- package/assess/lsa/examples/eval-run-5/gptoss-b/findings/priv-privilege-002.json +268 -0
- package/assess/lsa/examples/eval-run-5/gptoss-b/findings.json +3913 -0
- package/assess/lsa/examples/eval-run-5/gptoss-b/mass-assignment.candidates.json +232 -0
- package/assess/lsa/examples/eval-run-5/gptoss-b/routes/routes-001.json +425 -0
- package/assess/lsa/examples/eval-run-5/gptoss-b/routes/routes-002.json +333 -0
- package/assess/lsa/examples/eval-run-5/gptoss-b/routes/routes-003.json +400 -0
- package/assess/lsa/examples/eval-run-5/gptoss-b/routes/routes-004.json +403 -0
- package/assess/lsa/examples/eval-run-5/gptoss-b/routes/routes-005.json +398 -0
- package/assess/lsa/examples/eval-run-5/gptoss-b/routes/routes-006.json +63 -0
- package/assess/lsa/examples/eval-run-5/gptoss-b/score.json +94 -0
- package/assess/lsa/examples/eval-run-5/ground-truth.json +823 -0
- package/assess/lsa/knowledge/injection-patterns.md +8 -1
- package/assess/lsa/knowledge/stacks/nextjs.md +62 -0
- package/assess/lsa/prompts/35-privilege-auditor.md +3 -1
- package/assess/lsa/prompts/_shared-rules.md +1 -0
- package/assess/lsa/scripts/detect_stack.py +5 -1
- package/assess/lsa/scripts/find_injection.py +34 -5
- package/assess/lsa/scripts/lsa_common.py +9 -2
- package/assess/lsa/scripts/make_batches.py +2 -1
- package/assess/lsa/scripts/map_routes.py +10 -2
- package/assess/lsa/scripts/nextjs_routes.py +236 -0
- package/assess/lsa/templates/audit-config.json +1 -1
- package/assess/lsa/tests/EVAL-RESULTS.md +156 -0
- package/assess/lsa/tests/score_findings.py +58 -0
- package/assess/lsa/tests/test_default_scope.py +86 -0
- package/assess/lsa/tests/test_eval_run_4.py +73 -0
- package/assess/lsa/tests/test_eval_run_5.py +73 -0
- package/assess/lsa/tests/test_js_tagged_sql.py +112 -0
- package/assess/lsa/tests/test_nextjs.py +198 -0
- package/assess/lsa/tests/test_privilege_tier2.py +30 -0
- package/operate/ai/anchoring.py +130 -0
- package/operate/ai/client.py +13 -0
- package/operate/ai/context.py +105 -0
- package/operate/ai/runner.py +197 -69
- package/operate/cli/__init__.py +1 -1
- package/operate/cli/commands/scan.py +1 -1
- package/operate/tests/test_ai_anchoring.py +184 -0
- package/operate/tests/test_headless_ai.py +148 -1
- package/package.json +1 -1
|
@@ -57,7 +57,9 @@ Parameterization Cheat Sheet; CWE-89, CWE-564, CWE-943.
|
|
|
57
57
|
`selectall_arrayref`, `selectrow_*`.
|
|
58
58
|
- **ColdFusion**: `<cfquery>` body, `queryExecute("..." & x)`.
|
|
59
59
|
- **Node**: `connection.query`, `pool.query`, `sequelize.query`,
|
|
60
|
-
`knex.raw`, `db.collection(...).find/aggregate/$where
|
|
60
|
+
`knex.raw`, `db.collection(...).find/aggregate/$where`, Prisma
|
|
61
|
+
`$queryRawUnsafe` / `$executeRawUnsafe`, and raw fragments inside tagged
|
|
62
|
+
templates: Drizzle / postgres.js `sql.raw(x)`, `sql.unsafe(x)`, `Prisma.raw(x)`.
|
|
61
63
|
- **Python**: `cursor.execute`, `executemany`, `engine.execute`,
|
|
62
64
|
`Model.objects.raw`, `extra(where=...)`.
|
|
63
65
|
- **Ruby**: `find_by_sql`, `where("...#{}")`, `order(params[:sort])`,
|
|
@@ -80,6 +82,7 @@ Parameterization Cheat Sheet; CWE-89, CWE-564, CWE-943.
|
|
|
80
82
|
| Regex "no quotes" / blacklist (`select`, `union`) | No | Treat as unsanitized; mention in notes. |
|
|
81
83
|
| `magic_quotes_gpc` | No | Removed in PHP 5.4; never rely on it. |
|
|
82
84
|
| ORM entity methods (`find($id)`, `Where(x => x.Id == id)`) | Yes | Not raw. |
|
|
85
|
+
| JS tagged templates: Drizzle / postgres.js / Slonik `` sql`... ${x}` ``, `` Prisma.sql`...` ``, `` prisma.$queryRaw`...` `` | Yes | `${x}` is sent as a bound parameter. Not the same as an untagged template passed to `db.execute(...)` or `$queryRawUnsafe(...)`, and not `sql.raw(x)` / `Prisma.raw(x)` inside the template, which insert raw text. |
|
|
83
86
|
|
|
84
87
|
## 4. NoSQL specifics
|
|
85
88
|
|
|
@@ -115,6 +118,10 @@ A string that is built but **never sent to any sink** is not a finding: verdict
|
|
|
115
118
|
(`"IN (" + String.join(",", Collections.nCopies(n, "?")) + ")"`) — safe if
|
|
116
119
|
values are bound afterwards.
|
|
117
120
|
- Log or error messages that contain SQL keywords.
|
|
121
|
+
- `sql-raw-fragment` whose argument is a schema object or constant
|
|
122
|
+
(`sql.raw(`'${seq.seqName}'`)` with `seq = pgSequence("customer_code_seq")`)
|
|
123
|
+
— not attacker-controlled. The pre-scan marks these `priority: low` when no
|
|
124
|
+
request input is in the file.
|
|
118
125
|
- MyBatis `#{}` (safe) vs `${}` (unsafe) — do not confuse.
|
|
119
126
|
- Session-only values (`$_SESSION['uid']` set by the server at login) — not
|
|
120
127
|
attacker-controlled, but note if the session value itself came from an
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Next.js Knowledge Profile (App Router, Pages Router, Server Actions)
|
|
2
|
+
|
|
3
|
+
## Stack Identification
|
|
4
|
+
- **Markers:** `next.config.(js|mjs|ts)`, `"next"` in `package.json`, an `app/` or `src/app/` tree with
|
|
5
|
+
`route.ts` / `page.tsx`, a `pages/api/` tree, imports from `next/server`, `next/navigation`, `next/headers`.
|
|
6
|
+
- **Auth libraries:** Auth.js / NextAuth (`auth()`, `getServerSession`), Better Auth (`getSession`,
|
|
7
|
+
`getSessionCookie`), Clerk (`auth()`, `currentUser()`), Lucia, iron-session, or a project wrapper such as
|
|
8
|
+
`withAuth(handler, { permission })`.
|
|
9
|
+
- **Data access:** Drizzle (`db.query.x.findFirst`, `db.select().from(x).where(...)`), Prisma
|
|
10
|
+
(`prisma.x.findUnique`), postgres.js, `@vercel/postgres`, Mongoose.
|
|
11
|
+
|
|
12
|
+
## Entry points (what the pre-scan inventories)
|
|
13
|
+
| Kind | File | URL | Notes |
|
|
14
|
+
|---|---|---|---|
|
|
15
|
+
| Route handler | `app/**/route.ts` | folder path; `(group)` and `@slot` dropped, `[id]` → `{id}`, `[...x]` / `[[...x]]` → `{x*}` | One route per exported `GET`/`POST`/... function or `export const GET = wrap(...)`. `_folder` is private (not routed). |
|
|
16
|
+
| Pages API | `pages/api/**.ts` | `/api/...`, `index` dropped | One handler for all methods; method from `req.method` checks. |
|
|
17
|
+
| Server action | file starting with `"use server"`, or a function whose body starts with `"use server"` | `action:<file>#<name>` | **Every exported function is a public POST endpoint**, callable with any arguments, even if the UI never calls it that way. |
|
|
18
|
+
| Page | `app/**/page.tsx` with a dynamic segment or `searchParams` | folder path | A GET that renders data loaded by id. |
|
|
19
|
+
|
|
20
|
+
## Where the guard lives
|
|
21
|
+
1. **`middleware.ts` / `proxy.ts` (Next.js 16):** runs before routes listed in `config.matcher`. Usually only
|
|
22
|
+
checks that a session cookie exists; it rarely checks roles or ownership, and a matcher gap leaves routes
|
|
23
|
+
open. Never accept middleware alone as an ownership check.
|
|
24
|
+
2. **Per handler:** `const session = await auth(); if (!session) return 401`, or a wrapper (`withAuth`).
|
|
25
|
+
Check the wrapper once and reuse the result for every route that uses it.
|
|
26
|
+
3. **Server actions:** need their own check inside the action. Layout or page checks do **not** protect an
|
|
27
|
+
action, because the action can be called directly.
|
|
28
|
+
|
|
29
|
+
## BOLA / IDOR Patterns
|
|
30
|
+
- **Vulnerable:** id from `params` / `searchParams` / action argument / JSON body used without the user or
|
|
31
|
+
tenant in the query:
|
|
32
|
+
```ts
|
|
33
|
+
export async function GET(_req: Request, { params }: { params: Promise<{ id: string }> }) {
|
|
34
|
+
const { id } = await params;
|
|
35
|
+
const order = await db.query.orders.findFirst({ where: eq(orders.id, id) }); // any user's order
|
|
36
|
+
return Response.json(order);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
"use server";
|
|
40
|
+
export async function cancelOrder(orderId: number) {
|
|
41
|
+
const session = await auth(); // authenticated, but...
|
|
42
|
+
await db.update(orders).set({ status: "cancelled" }).where(eq(orders.id, orderId)); // ...not owner-scoped
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
- **Protected:** the session user or tenant is part of the lookup, or the loaded row is compared to it, and
|
|
46
|
+
a miss returns 404:
|
|
47
|
+
```ts
|
|
48
|
+
const order = await db.query.orders.findFirst({
|
|
49
|
+
where: and(eq(orders.id, id), eq(orders.tenantId, session.user.tenantId)),
|
|
50
|
+
});
|
|
51
|
+
if (!order) return new Response(null, { status: 404 });
|
|
52
|
+
```
|
|
53
|
+
- **Mass assignment:** `db.update(users).set(await req.json())` or spreading `formData` into `.set({...})`
|
|
54
|
+
lets a user write `role`, `tenantId`, `ownerId`.
|
|
55
|
+
- **BFLA:** admin routes guarded only by "is logged in" (middleware cookie check, `auth()` without a role or
|
|
56
|
+
permission check).
|
|
57
|
+
|
|
58
|
+
## SQL Injection Patterns
|
|
59
|
+
- **Safe:** Drizzle query builder, Drizzle / postgres.js / Prisma tagged templates (`` sql`... ${x}` ``,
|
|
60
|
+
`` prisma.$queryRaw`...` ``): `${x}` is a bound parameter.
|
|
61
|
+
- **Vulnerable:** `sql.raw(x)`, `sql.unsafe(x)`, `Prisma.raw(x)`, `$queryRawUnsafe(`...${x}`)`, or an untagged
|
|
62
|
+
template passed to `db.execute(...)`, with `x` from the request.
|
|
@@ -69,7 +69,9 @@ privileged, and a hinted route can be safe.
|
|
|
69
69
|
2. **Find the model's fields.** Read the model / entity / table. List the
|
|
70
70
|
privileged fields it holds (role, admin flag, permissions, owner, user,
|
|
71
71
|
tenant, price, amount, balance, status, verified, approved, plan).
|
|
72
|
-
No privileged field → `safe`, reason "no privileged field".
|
|
72
|
+
No privileged field → `safe`, reason "no privileged field". If you cannot
|
|
73
|
+
find or read the model's definition, you do not know its fields: answer
|
|
74
|
+
`needs_review` (reason "model fields not visible"), never `safe`.
|
|
73
75
|
3. **Look for what limits the fields**, with `file:line`: explicit field
|
|
74
76
|
copy, allow-list (`$fillable` without privileged fields, `permit(...)`,
|
|
75
77
|
`[Bind(Include=...)]`, DTO with only editable fields), validation that
|
|
@@ -84,6 +84,7 @@ Batch files list the right files in `knowledge_files`. If they do not:
|
|
|
84
84
|
| `classic-asp` | `knowledge/stacks/classic-asp.md` |
|
|
85
85
|
| `perl` | `knowledge/stacks/perl-cgi.md` |
|
|
86
86
|
| `coldfusion` | `knowledge/stacks/coldfusion.md` |
|
|
87
|
+
| `node-nextjs` | `knowledge/stacks/nextjs.md` |
|
|
87
88
|
| `node`, `python`, `ruby` | `knowledge/stacks/node-python-ruby.md` |
|
|
88
89
|
| `sql-procedures` | `knowledge/injection-patterns.md` §1 (stored procedures) |
|
|
89
90
|
|
|
@@ -61,6 +61,8 @@ RULES = [
|
|
|
61
61
|
("go-gorilla", "go", "go.md", [("text", r"github\.com/gorilla/mux|mux\.NewRouter")]),
|
|
62
62
|
("python-fastapi", "python", "node-python-ruby.md", [("text", r"from\s+fastapi\s+import|import\s+fastapi|FastAPI\s*\(")]),
|
|
63
63
|
("node-nestjs", "typescript", "node-python-ruby.md", [("text", r"@nestjs/(common|core|swagger)")]),
|
|
64
|
+
("node-nextjs", "typescript", "nextjs.md", [("name", r"(^|/)next\.config\.(js|mjs|cjs|ts|mts)$"),
|
|
65
|
+
("text", r"from\s+['\"]next/(server|navigation|headers|cache)['\"]")]),
|
|
64
66
|
]
|
|
65
67
|
|
|
66
68
|
DATA_ACCESS = [
|
|
@@ -85,6 +87,8 @@ DATA_ACCESS = [
|
|
|
85
87
|
("SQLAlchemy (Python)", r"sqlalchemy\.|create_engine|SessionLocal"),
|
|
86
88
|
("Prisma (Node/TS)", r"PrismaClient|@prisma/client"),
|
|
87
89
|
("TypeORM (Node/TS)", r"typeorm|@Entity|getRepository"),
|
|
90
|
+
("Drizzle ORM (Node/TS)", r"drizzle-orm"),
|
|
91
|
+
("postgres.js (Node/TS)", r"from\s+['\"]postgres['\"]|require\(\s*['\"]postgres['\"]\s*\)"),
|
|
88
92
|
]
|
|
89
93
|
|
|
90
94
|
DB_HINTS = [
|
|
@@ -94,7 +98,7 @@ DB_HINTS = [
|
|
|
94
98
|
]
|
|
95
99
|
|
|
96
100
|
TEXT_SAMPLE_EXTS = {".sql", ".php", ".inc", ".java", ".jsp", ".xml", ".cs", ".vb", ".aspx", ".asax", ".config", ".asp",
|
|
97
|
-
".pl", ".pm", ".cgi", ".cfm", ".cfc", ".js", ".mjs", ".ts", ".py", ".rb", ".kt", ".kts",
|
|
101
|
+
".pl", ".pm", ".cgi", ".cfm", ".cfc", ".js", ".mjs", ".ts", ".tsx", ".jsx", ".py", ".rb", ".kt", ".kts",
|
|
98
102
|
".rs", ".go", ".toml", ".mod"}
|
|
99
103
|
|
|
100
104
|
|
|
@@ -28,7 +28,7 @@ LANG_BY_EXT = {
|
|
|
28
28
|
".asp": "vb", ".asa": "vb",
|
|
29
29
|
".pl": "perl", ".pm": "perl", ".cgi": "perl",
|
|
30
30
|
".cfm": "cfml", ".cfc": "cfml", ".cfml": "cfml",
|
|
31
|
-
".js": "js", ".mjs": "js", ".ts": "js", ".py": "python", ".rb": "ruby",
|
|
31
|
+
".js": "js", ".mjs": "js", ".cjs": "js", ".mts": "js", ".ts": "js", ".tsx": "js", ".jsx": "js", ".py": "python", ".rb": "ruby",
|
|
32
32
|
".rs": "rust", ".go": "go",
|
|
33
33
|
".xml": "xml", ".sql": "sql",
|
|
34
34
|
}
|
|
@@ -108,6 +108,28 @@ ASSIGN = {
|
|
|
108
108
|
"rust": re.compile(r"\b(\w+)\s*(\+=|=)(?!=)"), "go": re.compile(r"\b(\w+)\s*(\+=|=)(?!=)"),
|
|
109
109
|
}
|
|
110
110
|
APPEND_CALL = re.compile(r"\b(\w+)\s*\.\s*(append|Append|AppendLine|concat)\s*\(")
|
|
111
|
+
# JS/TS tagged templates that bind ${...} as parameters: Drizzle / postgres.js / Slonik `sql`, Prisma.sql, $queryRaw
|
|
112
|
+
SQL_TAG = re.compile(r"(?:(?<![\w$])(?:[\w$]+\.)*sql|\$queryRaw|\$executeRaw)\s*(?:<[^`]*>)?\s*$")
|
|
113
|
+
# escape hatches that put raw text into a tagged template
|
|
114
|
+
RAW_FRAGMENT = re.compile(r"\b(?:sql\.raw|sql\.unsafe|Prisma\.raw)\s*\(")
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def untagged(line: str, lits: List[re.Match], lang: str) -> List[re.Match]:
|
|
118
|
+
"""Drop JS template literals whose tag binds interpolations as query parameters."""
|
|
119
|
+
if lang != "js":
|
|
120
|
+
return lits
|
|
121
|
+
return [m for m in lits if not (m.group(0).startswith("`") and SQL_TAG.search(line[:m.start()]))]
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def raw_fragment_arg_is_constant(arg: str) -> bool:
|
|
125
|
+
"""True when sql.raw(...) gets a plain string literal (no ${...})."""
|
|
126
|
+
arg = arg.lstrip()
|
|
127
|
+
if arg[:1] in ("'", '"'):
|
|
128
|
+
return True
|
|
129
|
+
if arg[:1] == "`":
|
|
130
|
+
end = arg.find("`", 1)
|
|
131
|
+
return end != -1 and "${" not in arg[1:end]
|
|
132
|
+
return False
|
|
111
133
|
|
|
112
134
|
|
|
113
135
|
def strip_literals(line: str, lit: re.Pattern) -> str:
|
|
@@ -188,7 +210,8 @@ class Finder:
|
|
|
188
210
|
self.seen = set()
|
|
189
211
|
self.suppressed: List[Dict] = []
|
|
190
212
|
|
|
191
|
-
def add(self, lang, rel, lineno, lines, rule, subtype, category, sql_var=None, file_has_input=False
|
|
213
|
+
def add(self, lang, rel, lineno, lines, rule, subtype, category, sql_var=None, file_has_input=False,
|
|
214
|
+
low_without_input=False):
|
|
192
215
|
key = (rel, lineno)
|
|
193
216
|
if key in self.seen:
|
|
194
217
|
return
|
|
@@ -209,7 +232,8 @@ class Finder:
|
|
|
209
232
|
"file": rel, "line": lineno, "snippet": redact(snippet(lines, lineno)),
|
|
210
233
|
"sql_var": sql_var, "input_hint": hint,
|
|
211
234
|
"sanitizer_hints": sorted(set(m.group(0)[:30] for m in SANITIZER_RE.finditer(ctx)))[:5],
|
|
212
|
-
"priority": "high" if hint in ("request_input_on_line", "request_input_nearby")
|
|
235
|
+
"priority": "high" if hint in ("request_input_on_line", "request_input_nearby")
|
|
236
|
+
else "low" if low_without_input and hint == "none" else "normal",
|
|
213
237
|
})
|
|
214
238
|
|
|
215
239
|
# ----------------------------------------------------------------------------------
|
|
@@ -224,7 +248,7 @@ class Finder:
|
|
|
224
248
|
for i, line in enumerate(lines, 1):
|
|
225
249
|
if com and com.search(line):
|
|
226
250
|
continue
|
|
227
|
-
lits = literals(line, lit)
|
|
251
|
+
lits = untagged(line, literals(line, lit), lang)
|
|
228
252
|
if not any(STRONG_SQL.search(m.group(0)) for m in lits):
|
|
229
253
|
continue
|
|
230
254
|
if assign_rx:
|
|
@@ -258,7 +282,7 @@ class Finder:
|
|
|
258
282
|
if heredoc_sql and INTERP[lang].search(line):
|
|
259
283
|
self.add(lang, rel, i, lines, "heredoc-interpolation", "string_interpolation", "sql", None, file_has_input)
|
|
260
284
|
continue
|
|
261
|
-
lits = literals(line, lit)
|
|
285
|
+
lits = untagged(line, literals(line, lit), lang)
|
|
262
286
|
sql_lits = [m for m in lits if STRONG_SQL.search(m.group(0))]
|
|
263
287
|
target_var = None
|
|
264
288
|
if assign_rx:
|
|
@@ -300,6 +324,11 @@ class Finder:
|
|
|
300
324
|
for i, line in enumerate(lines, 1):
|
|
301
325
|
if com and com.search(line):
|
|
302
326
|
continue
|
|
327
|
+
if lang == "js":
|
|
328
|
+
fm = RAW_FRAGMENT.search(line)
|
|
329
|
+
if fm and not raw_fragment_arg_is_constant(line[fm.end():]):
|
|
330
|
+
self.add(lang, rel, i, lines, "sql-raw-fragment", "raw_fragment", "sql", None, file_has_input,
|
|
331
|
+
low_without_input=True)
|
|
303
332
|
rm = RAW_APIS.search(line)
|
|
304
333
|
if rm:
|
|
305
334
|
arg = line[rm.end():].strip()
|
|
@@ -15,7 +15,14 @@ DEFAULT_CONFIG: Dict = {
|
|
|
15
15
|
"include": [],
|
|
16
16
|
"exclude": ["vendor/", "node_modules/", "bower_components/", "packages/", "bin/", "obj/",
|
|
17
17
|
"target/", "build/", "dist/", ".git/", ".svn/", ".hg/", "test/", "tests/", "spec/", "docs/",
|
|
18
|
-
|
|
18
|
+
# framework build output and caches
|
|
19
|
+
".next/", ".nuxt/", ".output/", ".svelte-kit/", ".vercel/", ".netlify/", ".turbo/",
|
|
20
|
+
".parcel-cache/", ".cache/", "out/", "storybook-static/",
|
|
21
|
+
# test and coverage output
|
|
22
|
+
"coverage/", "test-results/", "playwright-report/", ".nyc_output/",
|
|
23
|
+
# Python environments and caches
|
|
24
|
+
"__pycache__/", ".venv/", "venv/", ".tox/", ".mypy_cache/", ".pytest_cache/",
|
|
25
|
+
"*.min.js", "*jquery*.js", "*.map"],
|
|
19
26
|
"web_roots": [],
|
|
20
27
|
"file_endpoints": "all",
|
|
21
28
|
"max_file_kb": 1024,
|
|
@@ -41,7 +48,7 @@ SOURCE_EXTS = {
|
|
|
41
48
|
".asp", ".asa",
|
|
42
49
|
".pl", ".pm", ".cgi",
|
|
43
50
|
".cfm", ".cfc", ".cfml",
|
|
44
|
-
".js", ".mjs", ".ts", ".py", ".rb", ".erb", ".sql",
|
|
51
|
+
".js", ".mjs", ".cjs", ".mts", ".ts", ".tsx", ".jsx", ".py", ".rb", ".erb", ".sql",
|
|
45
52
|
".rs", ".go", ".toml", ".mod",
|
|
46
53
|
}
|
|
47
54
|
|
|
@@ -22,7 +22,8 @@ sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
|
22
22
|
from lsa_common import load_config, read_json, write_json # noqa: E402
|
|
23
23
|
|
|
24
24
|
STACK_KNOWLEDGE = [("php", "php.md"), ("java", "java.md"), ("dotnet", "dotnet.md"), ("classic-asp", "classic-asp.md"),
|
|
25
|
-
("perl", "perl-cgi.md"), ("coldfusion", "coldfusion.md"), ("node", "
|
|
25
|
+
("perl", "perl-cgi.md"), ("coldfusion", "coldfusion.md"), ("node-nextjs", "nextjs.md"),
|
|
26
|
+
("node", "node-python-ruby.md"),
|
|
26
27
|
("python", "node-python-ruby.md"), ("ruby", "node-python-ruby.md")]
|
|
27
28
|
LANG_KNOWLEDGE = {"php": "php.md", "java": "java.md", "xml": "java.md", "cs": "dotnet.md", "vb": "dotnet.md",
|
|
28
29
|
"aspx": "dotnet.md", "perl": "perl-cgi.md", "cfml": "coldfusion.md", "js": "node-python-ruby.md",
|
|
@@ -21,6 +21,7 @@ from typing import Dict, List, Optional, Tuple
|
|
|
21
21
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
22
22
|
from lsa_common import (KIT_VERSION, compile_id_patterns, is_id_name, iter_source_files, # noqa: E402
|
|
23
23
|
line_of, load_config, read_text, write_json)
|
|
24
|
+
from nextjs_routes import middleware_note, nextjs_declared # noqa: E402
|
|
24
25
|
from privilege import annotate_privilege # noqa: E402
|
|
25
26
|
|
|
26
27
|
# --------------------------------------------------------------------------------------
|
|
@@ -138,7 +139,7 @@ LANG_BY_EXT = {
|
|
|
138
139
|
".asp": "asp", ".asa": "asp",
|
|
139
140
|
".pl": "perl", ".pm": "perl", ".cgi": "perl",
|
|
140
141
|
".cfm": "cfml", ".cfc": "cfml", ".cfml": "cfml",
|
|
141
|
-
".js": "js", ".mjs": "js", ".ts": "js", ".py": "python", ".rb": "ruby",
|
|
142
|
+
".js": "js", ".mjs": "js", ".cjs": "js", ".mts": "js", ".ts": "js", ".tsx": "js", ".jsx": "js", ".py": "python", ".rb": "ruby",
|
|
142
143
|
".rs": "rust", ".go": "go",
|
|
143
144
|
}
|
|
144
145
|
|
|
@@ -764,7 +765,9 @@ def _scan_route_file_batch(args: Tuple[List[Tuple[str, str]], Dict, set, str]) -
|
|
|
764
765
|
declared = dotnet_declared(c, rel, t)
|
|
765
766
|
elif lang == "cfml" and ext == ".cfc":
|
|
766
767
|
declared = cfml_declared(c, rel, t)
|
|
767
|
-
elif lang
|
|
768
|
+
elif lang == "js":
|
|
769
|
+
declared = nextjs_declared(c, rel, t) or js_py_rb_declared(c, rel, t, lang)
|
|
770
|
+
elif lang in ("python", "ruby"):
|
|
768
771
|
declared = js_py_rb_declared(c, rel, t, lang)
|
|
769
772
|
elif lang == "rust":
|
|
770
773
|
declared = rust_declared(c, rel, t)
|
|
@@ -881,6 +884,11 @@ def map_routes(target: Path, config: Dict, stack_ids: Optional[set] = None, work
|
|
|
881
884
|
for n, it in enumerate(keep, 1):
|
|
882
885
|
it["route_id"] = f"R-{n:04d}"
|
|
883
886
|
c.items = keep
|
|
887
|
+
mw_note = middleware_note(target, [rel for _, rel in files], enc) \
|
|
888
|
+
if any(it["stack"] == "node-nextjs" for it in c.items) else None
|
|
889
|
+
for it in c.items:
|
|
890
|
+
if mw_note and it["stack"] == "node-nextjs":
|
|
891
|
+
it["notes"] = f"{it['notes']}; {mw_note}" if it["notes"] else mw_note
|
|
884
892
|
for it in c.items:
|
|
885
893
|
it["needs_audit_hint"] = bool(any(i.get("is_id_like") for i in it["id_params"]) or it["kind"] == "declared_route")
|
|
886
894
|
c.items = annotate_privilege(c.items, target, config)
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""Next.js route inventory for map_routes.py. Python 3.8+, standard library only.
|
|
2
|
+
|
|
3
|
+
Covers App Router route handlers (app/**/route.ts), dynamic server-component pages
|
|
4
|
+
(app/**/page.tsx), Pages Router API routes (pages/api/**), and server actions
|
|
5
|
+
(files that start with "use server", and inline "use server" functions).
|
|
6
|
+
The file path is the URL, so routes come from folder names, not from declarations.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Dict, List, Optional
|
|
13
|
+
|
|
14
|
+
from lsa_common import is_id_name, line_of, read_text
|
|
15
|
+
|
|
16
|
+
JS_EXT = r"(?:[cm]?[jt]s|[jt]sx)"
|
|
17
|
+
APP_FILE = re.compile(r"(?:^|/)app/(?:(.+)/)?(route|page)\." + JS_EXT + "$")
|
|
18
|
+
PAGES_API = re.compile(r"(?:^|/)pages/(api(?:/.+)?)\." + JS_EXT + "$")
|
|
19
|
+
METHODS = "GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS"
|
|
20
|
+
HANDLER_DECL = re.compile(r"^export\s+(?:async\s+)?function\s+(" + METHODS + r")\b|^export\s+const\s+(" + METHODS + r")\s*=", re.M)
|
|
21
|
+
HANDLER_REEXPORT = re.compile(r"\b\w+\s+as\s+(" + METHODS + r")\b")
|
|
22
|
+
TOP_LEVEL = re.compile(r"^(?:export|function|async|const|let|var|class)\b", re.M)
|
|
23
|
+
ACTION_EXPORT = re.compile(r"^export\s+(?:default\s+)?async\s+function\s+(\w+)\s*\(|^export\s+const\s+(\w+)\s*=\s*async\s*\(", re.M)
|
|
24
|
+
INLINE_ACTION = re.compile(r"\basync\s+function\s+(\w+)\s*\(|\b(?:const|let)\s+(\w+)\s*=\s*async\s*\(")
|
|
25
|
+
USE_SERVER_STMT = re.compile(r"\s*['\"]use server['\"]")
|
|
26
|
+
REQ_METHOD = re.compile(r"req\.method\s*===?\s*['\"](\w+)['\"]|case\s+['\"](" + METHODS + r")['\"]\s*:")
|
|
27
|
+
MIDDLEWARE_FILE = re.compile(r"^(?:src/)?(middleware|proxy)\." + JS_EXT + "$")
|
|
28
|
+
MATCHER = re.compile(r"matcher\s*:\s*(\[[^\]]*\]|['\"][^'\"]*['\"])")
|
|
29
|
+
|
|
30
|
+
QUERY_INPUT = re.compile(r"searchParams\??\.get(?:All)?\(\s*['\"]([\w-]+)['\"]")
|
|
31
|
+
FORM_INPUT = re.compile(r"\b\w*[fF]orm\w*\??\.get(?:All)?\(\s*['\"]([\w-]+)['\"]")
|
|
32
|
+
JSON_DESTRUCT = re.compile(r"\{([^{}]*)\}\s*=\s*(?:await\s+)?[\w.]+\.(?:json|formData)\(\)")
|
|
33
|
+
DESTRUCT_NAME = re.compile(r"(?:^|,)\s*(?:\.\.\.)?(\w+)")
|
|
34
|
+
|
|
35
|
+
GUARD_HINTS = [re.compile(p) for p in [
|
|
36
|
+
r"\bauth\s*\(\s*\)", r"\bgetServerSession\s*\(", r"\bget(?:Session|User|Auth|CurrentUser)\s*\(", r"\bcurrentUser\s*\(",
|
|
37
|
+
r"\b(?:with|require|ensure|assert)\w*(?:Auth|User|Session|Role|Permission|Admin)\w*\s*\(",
|
|
38
|
+
r"\bpermission\s*:\s*['\"][^'\"]+['\"]", r"\bhas(?:Role|Permission)\s*\(",
|
|
39
|
+
r"redirect\s*\(\s*['\"]/(?:login|signin|sign-in)",
|
|
40
|
+
]]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def route_path(folder: Optional[str], prefix: str = "") -> Optional[str]:
|
|
44
|
+
"""URL path for an app/ or pages/ folder; None for private (_folder) segments."""
|
|
45
|
+
parts = [prefix] if prefix else []
|
|
46
|
+
for seg in (folder or "").split("/"):
|
|
47
|
+
if not seg or seg.startswith("@") or (seg.startswith("(") and seg.endswith(")")):
|
|
48
|
+
continue # route group or parallel-route slot: not part of the URL
|
|
49
|
+
if seg.startswith("_"):
|
|
50
|
+
return None
|
|
51
|
+
seg = re.sub(r"^(?:\(\.+\))+", "", seg) # interception marker (.) (..) (...)
|
|
52
|
+
catch = re.fullmatch(r"\[\[?\.\.\.(\w+)\]\]?", seg)
|
|
53
|
+
dyn = re.fullmatch(r"\[(\w+)\]", seg)
|
|
54
|
+
parts.append("{" + catch.group(1) + "*}" if catch else "{" + dyn.group(1) + "}" if dyn else seg)
|
|
55
|
+
return "/" + "/".join(p for p in parts if p)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _close(text: str, idx: int, open_ch: str, close_ch: str) -> int:
|
|
59
|
+
"""Index of the bracket that closes text[idx] (or len(text))."""
|
|
60
|
+
depth = 0
|
|
61
|
+
for j in range(idx, len(text)):
|
|
62
|
+
if text[j] == open_ch:
|
|
63
|
+
depth += 1
|
|
64
|
+
elif text[j] == close_ch:
|
|
65
|
+
depth -= 1
|
|
66
|
+
if depth == 0:
|
|
67
|
+
return j
|
|
68
|
+
return len(text)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _starts_with_use_server(text: str) -> bool:
|
|
72
|
+
"""True when the first statement of the file is the "use server" directive (comments may come first)."""
|
|
73
|
+
i, n = 0, len(text)
|
|
74
|
+
while i < n:
|
|
75
|
+
if text[i].isspace():
|
|
76
|
+
i += 1
|
|
77
|
+
elif text.startswith("//", i):
|
|
78
|
+
nl = text.find("\n", i)
|
|
79
|
+
i = n if nl == -1 else nl + 1
|
|
80
|
+
elif text.startswith("/*", i):
|
|
81
|
+
end = text.find("*/", i + 2)
|
|
82
|
+
i = n if end == -1 else end + 2
|
|
83
|
+
else:
|
|
84
|
+
break
|
|
85
|
+
return bool(USE_SERVER_STMT.match(text, i))
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _segment_end(text: str, start: int) -> int:
|
|
89
|
+
nxt = TOP_LEVEL.search(text, text.find("\n", start) + 1 if "\n" in text[start:] else len(text))
|
|
90
|
+
return nxt.start() if nxt else len(text)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _signature_params(sig: str) -> List[str]:
|
|
94
|
+
"""Parameter names of an action signature; FormData parameters are read through .get() instead."""
|
|
95
|
+
names: List[str] = []
|
|
96
|
+
depth, cur, parts = 0, "", []
|
|
97
|
+
for ch in sig:
|
|
98
|
+
depth += ch in "({[<"
|
|
99
|
+
depth -= ch in ")}]>"
|
|
100
|
+
if ch == "," and depth == 0:
|
|
101
|
+
parts.append(cur)
|
|
102
|
+
cur = ""
|
|
103
|
+
else:
|
|
104
|
+
cur += ch
|
|
105
|
+
parts.append(cur)
|
|
106
|
+
for part in (p.strip() for p in parts):
|
|
107
|
+
if not part or re.search(r":\s*FormData\b", part):
|
|
108
|
+
continue
|
|
109
|
+
if part.startswith("{"):
|
|
110
|
+
names += DESTRUCT_NAME.findall(part[1:_close(part, 0, "{", "}")])
|
|
111
|
+
else:
|
|
112
|
+
m = re.match(r"(?:\.\.\.)?(\w+)", part)
|
|
113
|
+
if m:
|
|
114
|
+
names.append(m.group(1))
|
|
115
|
+
return names
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class NextRoutes:
|
|
119
|
+
def __init__(self, c, rel: str, text: str):
|
|
120
|
+
self.c, self.rel, self.text = c, rel, text
|
|
121
|
+
self.found = False
|
|
122
|
+
|
|
123
|
+
def _param(self, name: str, source: str, line: Optional[int]) -> Dict:
|
|
124
|
+
return {"name": name, "source": source, "line": line, "is_id_like": is_id_name(name, self.c.idp)}
|
|
125
|
+
|
|
126
|
+
def inputs(self, path: str, body: str, base_line: int, args: Optional[List[str]] = None) -> List[Dict]:
|
|
127
|
+
out = list(self.c.path_params(path)) if path.startswith("/") else []
|
|
128
|
+
out += [self._param(n, "body", base_line) for n in args or []]
|
|
129
|
+
for rx, src in ((QUERY_INPUT, "query"), (FORM_INPUT, "form")):
|
|
130
|
+
out += [self._param(m.group(1), src, base_line + line_of(body, m.start()) - 1) for m in rx.finditer(body)]
|
|
131
|
+
for m in JSON_DESTRUCT.finditer(body):
|
|
132
|
+
line = base_line + line_of(body, m.start()) - 1
|
|
133
|
+
out += [self._param(n, "body", line) for n in DESTRUCT_NAME.findall(m.group(1))]
|
|
134
|
+
out += self.c.inputs("js", body, base_line)
|
|
135
|
+
seen, dedup = set(), []
|
|
136
|
+
for p in out:
|
|
137
|
+
if p["name"] not in seen:
|
|
138
|
+
seen.add(p["name"])
|
|
139
|
+
dedup.append(p)
|
|
140
|
+
return dedup
|
|
141
|
+
|
|
142
|
+
def guards(self, body: str, base_line: int) -> List[Dict]:
|
|
143
|
+
out = self.c.guards(body, base_line)
|
|
144
|
+
for rx in GUARD_HINTS:
|
|
145
|
+
m = rx.search(body)
|
|
146
|
+
if m:
|
|
147
|
+
out.append({"hint": m.group(0)[:80], "line": base_line + line_of(body, m.start()) - 1})
|
|
148
|
+
seen, dedup = set(), []
|
|
149
|
+
for g in sorted(out, key=lambda g: g["line"]):
|
|
150
|
+
if (g["hint"], g["line"]) not in seen:
|
|
151
|
+
seen.add((g["hint"], g["line"]))
|
|
152
|
+
dedup.append(g)
|
|
153
|
+
return dedup[:8]
|
|
154
|
+
|
|
155
|
+
def add(self, kind: str, method: List[str], path: str, start: int, end: int, notes: str,
|
|
156
|
+
symbol: Optional[str] = None, args: Optional[List[str]] = None) -> None:
|
|
157
|
+
body = self.text[start:end]
|
|
158
|
+
line = line_of(self.text, start)
|
|
159
|
+
self.c.add(stack="node-nextjs", kind=kind, method=method, path=path,
|
|
160
|
+
handler={"file": self.rel, "line": line, "symbol": symbol},
|
|
161
|
+
id_params=self.inputs(path, body, line, args), guard_hints=self.guards(body, line),
|
|
162
|
+
data_hints=self.c.data_ops(body, line), notes=notes)
|
|
163
|
+
self.found = True
|
|
164
|
+
|
|
165
|
+
def route_handlers(self, path: str) -> None:
|
|
166
|
+
decls = list(HANDLER_DECL.finditer(self.text))
|
|
167
|
+
for i, m in enumerate(decls):
|
|
168
|
+
end = decls[i + 1].start() if i + 1 < len(decls) else len(self.text)
|
|
169
|
+
self.add("declared_route", [m.group(1) or m.group(2)], path, m.start(), end, "next.js route handler",
|
|
170
|
+
symbol=m.group(1) or m.group(2))
|
|
171
|
+
if not decls:
|
|
172
|
+
for m in HANDLER_REEXPORT.finditer(self.text):
|
|
173
|
+
self.add("declared_route", [m.group(1)], path, 0, len(self.text), "next.js route handler (re-exported)",
|
|
174
|
+
symbol=m.group(1))
|
|
175
|
+
|
|
176
|
+
def page(self, path: str) -> None:
|
|
177
|
+
if "{" in path or QUERY_INPUT.search(self.text) or "searchParams" in self.text:
|
|
178
|
+
self.add("file_endpoint", ["GET"], path, 0, len(self.text),
|
|
179
|
+
"next.js server component page: check data loaded for params/searchParams")
|
|
180
|
+
|
|
181
|
+
def pages_api(self, folder: str) -> None:
|
|
182
|
+
folder = re.sub(r"(?:^|/)index$", "", folder) or "api"
|
|
183
|
+
path = route_path(folder)
|
|
184
|
+
if path is None:
|
|
185
|
+
return
|
|
186
|
+
methods = sorted({(m.group(1) or m.group(2)).upper() for m in REQ_METHOD.finditer(self.text)}) or ["ANY"]
|
|
187
|
+
self.add("declared_route", methods, path, 0, len(self.text), "next.js pages/api route")
|
|
188
|
+
|
|
189
|
+
def server_actions(self) -> None:
|
|
190
|
+
if _starts_with_use_server(self.text):
|
|
191
|
+
for m in ACTION_EXPORT.finditer(self.text):
|
|
192
|
+
name = m.group(1) or m.group(2)
|
|
193
|
+
paren = m.end() - 1
|
|
194
|
+
args = _signature_params(self.text[paren + 1:_close(self.text, paren, "(", ")")])
|
|
195
|
+
self.add("declared_route", ["POST"], f"action:{self.rel}#{name}", m.start(),
|
|
196
|
+
_segment_end(self.text, m.start()), "next.js server action (callable by any client with its id)",
|
|
197
|
+
symbol=name, args=args)
|
|
198
|
+
return
|
|
199
|
+
for m in INLINE_ACTION.finditer(self.text):
|
|
200
|
+
paren = m.end() - 1
|
|
201
|
+
close = _close(self.text, paren, "(", ")")
|
|
202
|
+
brace = self.text.find("{", close)
|
|
203
|
+
if brace == -1 or not USE_SERVER_STMT.match(self.text, brace + 1):
|
|
204
|
+
continue
|
|
205
|
+
name = m.group(1) or m.group(2)
|
|
206
|
+
self.add("declared_route", ["POST"], f"action:{self.rel}#{name}", m.start(),
|
|
207
|
+
_close(self.text, brace, "{", "}") + 1, "next.js inline server action (closure values are sent to the client encrypted; arguments are not trusted)",
|
|
208
|
+
symbol=name, args=_signature_params(self.text[paren + 1:close]))
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def nextjs_declared(c, rel: str, text: str) -> bool:
|
|
212
|
+
"""Add Next.js routes found in one JS/TS file. Returns True when the file is a Next.js entry point."""
|
|
213
|
+
r = NextRoutes(c, rel, text)
|
|
214
|
+
am = APP_FILE.search(rel)
|
|
215
|
+
if am:
|
|
216
|
+
path = route_path(am.group(1))
|
|
217
|
+
if path is not None:
|
|
218
|
+
if am.group(2) == "route":
|
|
219
|
+
r.route_handlers(path)
|
|
220
|
+
else:
|
|
221
|
+
r.page(path)
|
|
222
|
+
pm = PAGES_API.search(rel)
|
|
223
|
+
if pm:
|
|
224
|
+
r.pages_api(pm.group(1))
|
|
225
|
+
r.server_actions()
|
|
226
|
+
return r.found or bool(am or pm)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def middleware_note(target: Path, rels: List[str], enc) -> Optional[str]:
|
|
230
|
+
"""Describe middleware.ts / proxy.ts (Next.js 16) so the auditor checks it covers each route."""
|
|
231
|
+
for rel in rels:
|
|
232
|
+
if MIDDLEWARE_FILE.match(rel):
|
|
233
|
+
m = MATCHER.search(read_text(target / rel, enc))
|
|
234
|
+
scope = f"matcher: {m.group(1)[:120]}" if m else "no matcher: runs on every route"
|
|
235
|
+
return f"{rel} runs before this route ({scope}); confirm it enforces auth here"
|
|
236
|
+
return None
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scope": {
|
|
10
10
|
"include": [],
|
|
11
|
-
"exclude": ["vendor/", "node_modules/", "bower_components/", "packages/", "bin/", "obj/", "target/", "build/", "dist/", ".git/", "test/", "tests/", "spec/", "docs/", "*.min.js", "*jquery*.js"],
|
|
11
|
+
"exclude": ["vendor/", "node_modules/", "bower_components/", "packages/", "bin/", "obj/", "target/", "build/", "dist/", ".git/", ".svn/", ".hg/", "test/", "tests/", "spec/", "docs/", ".next/", ".nuxt/", ".output/", ".svelte-kit/", ".vercel/", ".netlify/", ".turbo/", ".parcel-cache/", ".cache/", "out/", "storybook-static/", "coverage/", "test-results/", "playwright-report/", ".nyc_output/", "__pycache__/", ".venv/", "venv/", ".tox/", ".mypy_cache/", ".pytest_cache/", "*.min.js", "*jquery*.js", "*.map"],
|
|
12
12
|
"web_roots": [],
|
|
13
13
|
"_file_endpoints": "all = every reachable page file is a candidate (recommended); with_inputs = only pages with request input or data access",
|
|
14
14
|
"file_endpoints": "all",
|