@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.
Files changed (143) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/README.md +1 -1
  3. package/assess/lsa/examples/eval-run-4/ground-truth.json +823 -0
  4. package/assess/lsa/examples/eval-run-4/iter0-gptoss-a/ai-run-stats.json +58 -0
  5. package/assess/lsa/examples/eval-run-4/iter0-gptoss-a/coverage.json +85 -0
  6. package/assess/lsa/examples/eval-run-4/iter0-gptoss-a/findings/priv-privilege-001.json +822 -0
  7. package/assess/lsa/examples/eval-run-4/iter0-gptoss-a/findings/priv-privilege-002.json +393 -0
  8. package/assess/lsa/examples/eval-run-4/iter0-gptoss-a/findings.json +4477 -0
  9. package/assess/lsa/examples/eval-run-4/iter0-gptoss-a/mass-assignment.candidates.json +232 -0
  10. package/assess/lsa/examples/eval-run-4/iter0-gptoss-a/routes/routes-001.json +425 -0
  11. package/assess/lsa/examples/eval-run-4/iter0-gptoss-a/routes/routes-002.json +333 -0
  12. package/assess/lsa/examples/eval-run-4/iter0-gptoss-a/routes/routes-003.json +400 -0
  13. package/assess/lsa/examples/eval-run-4/iter0-gptoss-a/routes/routes-004.json +403 -0
  14. package/assess/lsa/examples/eval-run-4/iter0-gptoss-a/routes/routes-005.json +398 -0
  15. package/assess/lsa/examples/eval-run-4/iter0-gptoss-a/routes/routes-006.json +63 -0
  16. package/assess/lsa/examples/eval-run-4/iter0-gptoss-a/score.json +120 -0
  17. package/assess/lsa/examples/eval-run-4/iter1-gptoss-a/ai-run-stats.json +55 -0
  18. package/assess/lsa/examples/eval-run-4/iter1-gptoss-a/coverage.json +50 -0
  19. package/assess/lsa/examples/eval-run-4/iter1-gptoss-a/findings/priv-privilege-001.json +613 -0
  20. package/assess/lsa/examples/eval-run-4/iter1-gptoss-a/findings/priv-privilege-002.json +287 -0
  21. package/assess/lsa/examples/eval-run-4/iter1-gptoss-a/findings.json +4870 -0
  22. package/assess/lsa/examples/eval-run-4/iter1-gptoss-a/mass-assignment.candidates.json +232 -0
  23. package/assess/lsa/examples/eval-run-4/iter1-gptoss-a/routes/routes-001.json +425 -0
  24. package/assess/lsa/examples/eval-run-4/iter1-gptoss-a/routes/routes-002.json +333 -0
  25. package/assess/lsa/examples/eval-run-4/iter1-gptoss-a/routes/routes-003.json +400 -0
  26. package/assess/lsa/examples/eval-run-4/iter1-gptoss-a/routes/routes-004.json +403 -0
  27. package/assess/lsa/examples/eval-run-4/iter1-gptoss-a/routes/routes-005.json +398 -0
  28. package/assess/lsa/examples/eval-run-4/iter1-gptoss-a/routes/routes-006.json +63 -0
  29. package/assess/lsa/examples/eval-run-4/iter1-gptoss-a/score.json +107 -0
  30. package/assess/lsa/examples/eval-run-4/iter1-gptoss-b/ai-run-stats.json +58 -0
  31. package/assess/lsa/examples/eval-run-4/iter1-gptoss-b/coverage.json +92 -0
  32. package/assess/lsa/examples/eval-run-4/iter1-gptoss-b/findings/priv-privilege-001.json +645 -0
  33. package/assess/lsa/examples/eval-run-4/iter1-gptoss-b/findings/priv-privilege-002.json +222 -0
  34. package/assess/lsa/examples/eval-run-4/iter1-gptoss-b/findings.json +4859 -0
  35. package/assess/lsa/examples/eval-run-4/iter1-gptoss-b/mass-assignment.candidates.json +232 -0
  36. package/assess/lsa/examples/eval-run-4/iter1-gptoss-b/routes/routes-001.json +425 -0
  37. package/assess/lsa/examples/eval-run-4/iter1-gptoss-b/routes/routes-002.json +333 -0
  38. package/assess/lsa/examples/eval-run-4/iter1-gptoss-b/routes/routes-003.json +400 -0
  39. package/assess/lsa/examples/eval-run-4/iter1-gptoss-b/routes/routes-004.json +403 -0
  40. package/assess/lsa/examples/eval-run-4/iter1-gptoss-b/routes/routes-005.json +398 -0
  41. package/assess/lsa/examples/eval-run-4/iter1-gptoss-b/routes/routes-006.json +63 -0
  42. package/assess/lsa/examples/eval-run-4/iter1-gptoss-b/score.json +121 -0
  43. package/assess/lsa/examples/eval-run-4/iter1-qwen-a-incomplete/coverage.json +111 -0
  44. package/assess/lsa/examples/eval-run-4/iter1-qwen-a-incomplete/findings/priv-privilege-001.json +6 -0
  45. package/assess/lsa/examples/eval-run-4/iter1-qwen-a-incomplete/findings/priv-privilege-002.json +344 -0
  46. package/assess/lsa/examples/eval-run-4/iter1-qwen-a-incomplete/findings.json +3544 -0
  47. package/assess/lsa/examples/eval-run-4/iter1-qwen-a-incomplete/mass-assignment.candidates.json +232 -0
  48. package/assess/lsa/examples/eval-run-4/iter1-qwen-a-incomplete/routes/routes-001.json +425 -0
  49. package/assess/lsa/examples/eval-run-4/iter1-qwen-a-incomplete/routes/routes-002.json +333 -0
  50. package/assess/lsa/examples/eval-run-4/iter1-qwen-a-incomplete/routes/routes-003.json +400 -0
  51. package/assess/lsa/examples/eval-run-4/iter1-qwen-a-incomplete/routes/routes-004.json +403 -0
  52. package/assess/lsa/examples/eval-run-4/iter1-qwen-a-incomplete/routes/routes-005.json +398 -0
  53. package/assess/lsa/examples/eval-run-4/iter1-qwen-a-incomplete/routes/routes-006.json +63 -0
  54. package/assess/lsa/examples/eval-run-4/iter1-qwen-a-incomplete/score.json +123 -0
  55. package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/ai-run-stats.json +50 -0
  56. package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/coverage.json +42 -0
  57. package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/findings/priv-privilege-001.json +540 -0
  58. package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/findings/priv-privilege-002.json +238 -0
  59. package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/findings.json +4383 -0
  60. package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/findings.sarif +8729 -0
  61. package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/mass-assignment.candidates.json +232 -0
  62. package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/report.ja.md +1911 -0
  63. package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/report.md +1911 -0
  64. package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/routes/routes-001.json +425 -0
  65. package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/routes/routes-002.json +333 -0
  66. package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/routes/routes-003.json +400 -0
  67. package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/routes/routes-004.json +403 -0
  68. package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/routes/routes-005.json +398 -0
  69. package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/routes/routes-006.json +63 -0
  70. package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/score.json +101 -0
  71. package/assess/lsa/examples/eval-run-4/iter2-gptoss-a/stack-profile.json +226 -0
  72. package/assess/lsa/examples/eval-run-4/iter2-gptoss-b/ai-run-stats.json +54 -0
  73. package/assess/lsa/examples/eval-run-4/iter2-gptoss-b/coverage.json +66 -0
  74. package/assess/lsa/examples/eval-run-4/iter2-gptoss-b/findings/priv-privilege-001.json +440 -0
  75. package/assess/lsa/examples/eval-run-4/iter2-gptoss-b/findings/priv-privilege-002.json +229 -0
  76. package/assess/lsa/examples/eval-run-4/iter2-gptoss-b/findings.json +3424 -0
  77. package/assess/lsa/examples/eval-run-4/iter2-gptoss-b/mass-assignment.candidates.json +232 -0
  78. package/assess/lsa/examples/eval-run-4/iter2-gptoss-b/routes/routes-001.json +425 -0
  79. package/assess/lsa/examples/eval-run-4/iter2-gptoss-b/routes/routes-002.json +333 -0
  80. package/assess/lsa/examples/eval-run-4/iter2-gptoss-b/routes/routes-003.json +400 -0
  81. package/assess/lsa/examples/eval-run-4/iter2-gptoss-b/routes/routes-004.json +403 -0
  82. package/assess/lsa/examples/eval-run-4/iter2-gptoss-b/routes/routes-005.json +398 -0
  83. package/assess/lsa/examples/eval-run-4/iter2-gptoss-b/routes/routes-006.json +63 -0
  84. package/assess/lsa/examples/eval-run-4/iter2-gptoss-b/score.json +120 -0
  85. package/assess/lsa/examples/eval-run-5/gptoss-a/ai-run-stats.json +88 -0
  86. package/assess/lsa/examples/eval-run-5/gptoss-a/coverage.json +36 -0
  87. package/assess/lsa/examples/eval-run-5/gptoss-a/findings/priv-privilege-001.json +530 -0
  88. package/assess/lsa/examples/eval-run-5/gptoss-a/findings/priv-privilege-002.json +248 -0
  89. package/assess/lsa/examples/eval-run-5/gptoss-a/findings.json +4766 -0
  90. package/assess/lsa/examples/eval-run-5/gptoss-a/findings.sarif +9326 -0
  91. package/assess/lsa/examples/eval-run-5/gptoss-a/mass-assignment.candidates.json +232 -0
  92. package/assess/lsa/examples/eval-run-5/gptoss-a/report.ja.md +2011 -0
  93. package/assess/lsa/examples/eval-run-5/gptoss-a/report.md +2011 -0
  94. package/assess/lsa/examples/eval-run-5/gptoss-a/routes/routes-001.json +425 -0
  95. package/assess/lsa/examples/eval-run-5/gptoss-a/routes/routes-002.json +333 -0
  96. package/assess/lsa/examples/eval-run-5/gptoss-a/routes/routes-003.json +400 -0
  97. package/assess/lsa/examples/eval-run-5/gptoss-a/routes/routes-004.json +403 -0
  98. package/assess/lsa/examples/eval-run-5/gptoss-a/routes/routes-005.json +398 -0
  99. package/assess/lsa/examples/eval-run-5/gptoss-a/routes/routes-006.json +63 -0
  100. package/assess/lsa/examples/eval-run-5/gptoss-a/score.json +80 -0
  101. package/assess/lsa/examples/eval-run-5/gptoss-a/stack-profile.json +226 -0
  102. package/assess/lsa/examples/eval-run-5/gptoss-b/ai-run-stats.json +83 -0
  103. package/assess/lsa/examples/eval-run-5/gptoss-b/coverage.json +44 -0
  104. package/assess/lsa/examples/eval-run-5/gptoss-b/findings/priv-privilege-001.json +579 -0
  105. package/assess/lsa/examples/eval-run-5/gptoss-b/findings/priv-privilege-002.json +268 -0
  106. package/assess/lsa/examples/eval-run-5/gptoss-b/findings.json +3913 -0
  107. package/assess/lsa/examples/eval-run-5/gptoss-b/mass-assignment.candidates.json +232 -0
  108. package/assess/lsa/examples/eval-run-5/gptoss-b/routes/routes-001.json +425 -0
  109. package/assess/lsa/examples/eval-run-5/gptoss-b/routes/routes-002.json +333 -0
  110. package/assess/lsa/examples/eval-run-5/gptoss-b/routes/routes-003.json +400 -0
  111. package/assess/lsa/examples/eval-run-5/gptoss-b/routes/routes-004.json +403 -0
  112. package/assess/lsa/examples/eval-run-5/gptoss-b/routes/routes-005.json +398 -0
  113. package/assess/lsa/examples/eval-run-5/gptoss-b/routes/routes-006.json +63 -0
  114. package/assess/lsa/examples/eval-run-5/gptoss-b/score.json +94 -0
  115. package/assess/lsa/examples/eval-run-5/ground-truth.json +823 -0
  116. package/assess/lsa/knowledge/injection-patterns.md +8 -1
  117. package/assess/lsa/knowledge/stacks/nextjs.md +62 -0
  118. package/assess/lsa/prompts/35-privilege-auditor.md +3 -1
  119. package/assess/lsa/prompts/_shared-rules.md +1 -0
  120. package/assess/lsa/scripts/detect_stack.py +5 -1
  121. package/assess/lsa/scripts/find_injection.py +34 -5
  122. package/assess/lsa/scripts/lsa_common.py +9 -2
  123. package/assess/lsa/scripts/make_batches.py +2 -1
  124. package/assess/lsa/scripts/map_routes.py +10 -2
  125. package/assess/lsa/scripts/nextjs_routes.py +236 -0
  126. package/assess/lsa/templates/audit-config.json +1 -1
  127. package/assess/lsa/tests/EVAL-RESULTS.md +156 -0
  128. package/assess/lsa/tests/score_findings.py +58 -0
  129. package/assess/lsa/tests/test_default_scope.py +86 -0
  130. package/assess/lsa/tests/test_eval_run_4.py +73 -0
  131. package/assess/lsa/tests/test_eval_run_5.py +73 -0
  132. package/assess/lsa/tests/test_js_tagged_sql.py +112 -0
  133. package/assess/lsa/tests/test_nextjs.py +198 -0
  134. package/assess/lsa/tests/test_privilege_tier2.py +30 -0
  135. package/operate/ai/anchoring.py +130 -0
  136. package/operate/ai/client.py +13 -0
  137. package/operate/ai/context.py +105 -0
  138. package/operate/ai/runner.py +197 -69
  139. package/operate/cli/__init__.py +1 -1
  140. package/operate/cli/commands/scan.py +1 -1
  141. package/operate/tests/test_ai_anchoring.py +184 -0
  142. package/operate/tests/test_headless_ai.py +148 -1
  143. 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") else "normal",
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
- "*.min.js", "*jquery*.js"],
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", "node-python-ruby.md"),
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 in ("js", "python", "ruby"):
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",