@sitar_fiercer4c/skills 0.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/LICENSE +5 -0
- package/README.md +75 -0
- package/bin/install.js +45 -0
- package/package.json +29 -0
- package/skills/architecture-walkthrough/SKILL.md +223 -0
- package/skills/architecture-walkthrough/references/sections.md +29 -0
- package/skills/architecture-walkthrough/scripts/check_structure.py +200 -0
- package/skills/autotest-webapp-ui/SKILL.md +58 -0
- package/skills/backend-code-review/SKILL.md +386 -0
- package/skills/backend-code-review/references/report-format.md +333 -0
- package/skills/backend-code-review/scripts/list_routes.py +269 -0
- package/skills/backend-code-review/scripts/sweep.py +550 -0
- package/skills/backend-code-review/scripts/verify_citations.py +201 -0
- package/skills/be-brief/SKILL.md +18 -0
- package/skills/clarke-list-excel/SKILL.md +51 -0
- package/skills/clarke-list-excel/references/output-schema.md +125 -0
- package/skills/clarke-list-excel/scripts/clarke_common.py +251 -0
- package/skills/clarke-list-excel/scripts/clarke_extract.py +487 -0
- package/skills/clarke-list-excel/scripts/load_clarke.py +322 -0
- package/skills/clarke-list-excel/scripts/run_all.py +63 -0
- package/skills/datalab-api/SKILL.md +163 -0
- package/skills/datalab-api/references/parameters-and-payload.md +121 -0
- package/skills/datalab-api/references/table-selection.md +35 -0
- package/skills/datalab-api/scripts/datalab_tables.py +365 -0
- package/skills/find-test-seam/SKILL.md +41 -0
- package/skills/frontend-code-review/SKILL.md +247 -0
- package/skills/frontend-code-review-2/SKILL.md +192 -0
- package/skills/frontend-code-review-2/scripts/fetch_pr_comments.py +65 -0
- package/skills/frontend-code-review-2/scripts/render_report.py +139 -0
- package/skills/murtaza-breif/SKILL.md +143 -0
- package/skills/murtaza-breif/scripts/save_brief.py +128 -0
- package/skills/pdf-to-json/SKILL.md +42 -0
- package/skills/pdf-to-json/references/output-schema.md +168 -0
- package/skills/pdf-to-json/scripts/extract_figures.py +319 -0
- package/skills/pdf-to-json/scripts/load_mongo.py +287 -0
- package/skills/pdf-to-json/scripts/pdf_extract.py +1313 -0
- package/skills/record-api-traffic/SKILL.md +434 -0
- package/skills/record-api-traffic/references/reading-recordings.md +224 -0
- package/skills/record-api-traffic/scripts/check-schema.mjs +184 -0
- package/skills/record-api-traffic/scripts/dump-quotation.mjs +67 -0
- package/skills/record-api-traffic/scripts/dump-source-excel.mjs +75 -0
- package/skills/record-api-traffic/scripts/lib/repo.mjs +109 -0
- package/skills/record-api-traffic/scripts/preflight.py +528 -0
- package/skills/record-api-traffic/scripts/record-api-traffic.py +720 -0
- package/skills/refac-wrt-business-goal/SKILL.md +305 -0
- package/skills/refac-wrt-business-goal/references/critic.md +170 -0
- package/skills/system-resource-triage/SKILL.md +180 -0
- package/skills/system-resource-triage/scripts/reap.sh +116 -0
- package/skills/system-resource-triage/scripts/triage.sh +111 -0
- package/skills/using-git-worktrees/SKILL.md +167 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
# Report format
|
|
2
|
+
|
|
3
|
+
The shape below is the output contract. It came out of three rounds of a reader reacting to
|
|
4
|
+
drafts, and each part of it earns its place โ the notes explain what each is for so you can
|
|
5
|
+
tell when a deviation is reasonable.
|
|
6
|
+
|
|
7
|
+
## Contents
|
|
8
|
+
|
|
9
|
+
- [Skeleton](#skeleton)
|
|
10
|
+
- [Severity model](#severity-model)
|
|
11
|
+
- [The MVCS skeleton](#the-mvcs-skeleton)
|
|
12
|
+
- [Finding tables](#finding-tables)
|
|
13
|
+
- [What the Location column asserts](#what-the-location-column-asserts)
|
|
14
|
+
- [Rows whose claim names a set](#rows-whose-claim-names-a-set)
|
|
15
|
+
- [Two things every repair does](#two-things-every-repair-does)
|
|
16
|
+
- [Worked example rows](#worked-example-rows)
|
|
17
|
+
- [Inventory tables are transcribed, not typed](#inventory-tables-are-transcribed-not-typed)
|
|
18
|
+
- [Closing sections](#closing-sections)
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Skeleton
|
|
23
|
+
|
|
24
|
+
````markdown
|
|
25
|
+
# Route Review โ <scope, e.g. "full HTTP surface" or "POST surface">
|
|
26
|
+
|
|
27
|
+
**Target:** <absolute path to the backend root>
|
|
28
|
+
**Rules applied:** <path to code-rules.md> โ <one-line paraphrase of the layering it mandates>
|
|
29
|
+
**Scope:** all N routes across the M route files, plus the K routes defined inline in `<entrypoint>`.
|
|
30
|
+
|
|
31
|
+
| Severity | Count | Meaning |
|
|
32
|
+
|---|---|---|
|
|
33
|
+
| ๐ด | N | Rule violation, security defect, or correctness bug |
|
|
34
|
+
| ๐ก | N | Rule drift, duplication, fragile coupling |
|
|
35
|
+
| โ ๏ธ | N | Observation / nit / inconsistency |
|
|
36
|
+
| โ
| N | Verified correct โ worth keeping as-is |
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Route inventory
|
|
41
|
+
|
|
42
|
+
| # | Method | Path (as mounted) | File:line | Auth | Validator |
|
|
43
|
+
|---|---|---|---|---|---|
|
|
44
|
+
| 1 | POST | `/api/auth/register` | `auth.routes.js:13` | โ | โ
|
|
|
45
|
+
...
|
|
46
|
+
|
|
47
|
+
**<X> of <N> routes have a validator.** <One sentence naming where they cluster, if they do.>
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Config
|
|
52
|
+
*Methodology not yet established โ this section was not reviewed.*
|
|
53
|
+
|
|
54
|
+
## Controllers
|
|
55
|
+
*Methodology not yet established โ this section was not reviewed.*
|
|
56
|
+
|
|
57
|
+
## Middleware
|
|
58
|
+
*Methodology not yet established โ this section was not reviewed.*
|
|
59
|
+
|
|
60
|
+
## Models
|
|
61
|
+
*Methodology not yet established โ this section was not reviewed.*
|
|
62
|
+
|
|
63
|
+
## Routes
|
|
64
|
+
|
|
65
|
+
### `src/routes/<file>.js`
|
|
66
|
+
|
|
67
|
+
> <Any file-level fact that changes how every route in it reads โ most often the mount
|
|
68
|
+
> prefix, when it differs from what the file's own comments claim.>
|
|
69
|
+
|
|
70
|
+
#### `<METHOD> <path>` โ line N
|
|
71
|
+
`<middleware chain> โ <controller>.<handler>`
|
|
72
|
+
|
|
73
|
+
| Sev | Rule | Location | Finding |
|
|
74
|
+
|---|---|---|---|
|
|
75
|
+
| ๐ด | <code-rules section> | `file.js:LINE` | <finding> |
|
|
76
|
+
|
|
77
|
+
### Routes defined in `src/<entrypoint>`
|
|
78
|
+
...
|
|
79
|
+
|
|
80
|
+
## Services
|
|
81
|
+
*Methodology not yet established โ this section was not reviewed.*
|
|
82
|
+
|
|
83
|
+
## Utils
|
|
84
|
+
*Methodology not yet established โ this section was not reviewed.*
|
|
85
|
+
|
|
86
|
+
## Validators
|
|
87
|
+
*Methodology not yet established โ this section was not reviewed.*
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## Cross-cutting
|
|
92
|
+
|
|
93
|
+
| Sev | Rule | Location | Finding |
|
|
94
|
+
|---|---|---|---|
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Highest-value fixes, in order
|
|
99
|
+
|
|
100
|
+
1. `file.js:LINE` โ <what and why it ranks here>
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Notes on inputs
|
|
105
|
+
|
|
106
|
+
<Anything true about a prior review document, or about the sweep/inventory scripts' own output,
|
|
107
|
+
that a reader of this review needs. Omit the section if there is nothing. Never a finding row.>
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Files read for this review
|
|
112
|
+
|
|
113
|
+
<list>
|
|
114
|
+
|
|
115
|
+
Not read (referenced only): <list>
|
|
116
|
+
````
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## Severity model
|
|
121
|
+
|
|
122
|
+
| Marker | Means |
|
|
123
|
+
|---|---|
|
|
124
|
+
| ๐ด | Rule violation, security defect, or correctness bug |
|
|
125
|
+
| ๐ก | Rule drift, duplication, fragile coupling |
|
|
126
|
+
| โ ๏ธ | Observation, nit, inconsistency |
|
|
127
|
+
| โ
| Verified correct โ worth keeping as-is |
|
|
128
|
+
|
|
129
|
+
Put the counts at the top. A reader deciding whether to read 400 lines of tables wants to know
|
|
130
|
+
the shape of what is coming, and the counts are also the honest headline โ a review with three
|
|
131
|
+
๐ด and forty โ ๏ธ is a different message than the reverse, and burying that lets a reader
|
|
132
|
+
mistake volume for severity.
|
|
133
|
+
|
|
134
|
+
Count as you write rather than estimating up front; the tally has to match the tables or the
|
|
135
|
+
whole document loses credibility on the first thing the reader can check.
|
|
136
|
+
|
|
137
|
+
**`โ
` rows are not padding.** A review listing only faults gives the reader no way to
|
|
138
|
+
distinguish "examined and sound" from "not examined". That ambiguity matters most exactly where
|
|
139
|
+
the stakes are highest โ an auth middleware with no findings against it could mean it is
|
|
140
|
+
correct or that nobody read it. Say which. It also makes the review usable as a record of what
|
|
141
|
+
was verified, and it stops a correct-but-unusual construction from being "fixed" later by
|
|
142
|
+
someone who assumes it was overlooked.
|
|
143
|
+
|
|
144
|
+
Aim to record the non-obvious correct decisions, not every line that works: a security control
|
|
145
|
+
that is properly closed, a guard placed before expensive work, an idempotency mechanism, a
|
|
146
|
+
deliberate departure from a convention that turns out to be right.
|
|
147
|
+
|
|
148
|
+
A `โ
` row names the failure path it followed and what it found there โ the catch block, the
|
|
149
|
+
concurrent second caller, the missing-config case. The happy path working is the default, not
|
|
150
|
+
evidence. Because the marker promises "examined and sound", it is the one row type where being
|
|
151
|
+
wrong is worse than being absent: it stops the next reader from looking. Keep its scope to what
|
|
152
|
+
you actually probed and do not let it spill onto adjacent code โ a careful streaming mechanism
|
|
153
|
+
can sit inside a `catch` that flattens every error to `404`, and a `โ
` on the former reads as
|
|
154
|
+
absolution for the latter.
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## The MVCS skeleton
|
|
159
|
+
|
|
160
|
+
The report's H2 headings come from the section list in the target repo's `code-rules.md`, in the
|
|
161
|
+
order that file lists them โ not from a fixed list in this skill. A repo whose rules file names
|
|
162
|
+
`jobs` or `adapters` gets those headings.
|
|
163
|
+
|
|
164
|
+
Every section that was not reviewed still gets its heading, carrying:
|
|
165
|
+
|
|
166
|
+
```markdown
|
|
167
|
+
*Methodology not yet established โ this section was not reviewed.*
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
This is the point of the skeleton. Without those lines a reader cannot tell an unreviewed
|
|
171
|
+
section from a clean one, and silence reads as a pass. With them, the report doubles as a map of
|
|
172
|
+
how much of the architecture has been covered so far.
|
|
173
|
+
|
|
174
|
+
---
|
|
175
|
+
|
|
176
|
+
## Finding tables
|
|
177
|
+
|
|
178
|
+
One H3 per route file, one H4 per route, one table per route.
|
|
179
|
+
|
|
180
|
+
Under each H4, restate the middleware chain as `middleware โ controller.handler`. It takes one
|
|
181
|
+
line and it means a reader can see the route's full shape without opening the file.
|
|
182
|
+
|
|
183
|
+
Columns:
|
|
184
|
+
|
|
185
|
+
| Column | Contents |
|
|
186
|
+
|---|---|
|
|
187
|
+
| **Sev** | One marker. |
|
|
188
|
+
| **Rule** | The `code-rules.md` section the finding is judged against โ `routes`, `controllers`, `services`, `validators`, `services (DONTS)`. Use `โ` when the finding is a security or correctness issue rather than a layering one. |
|
|
189
|
+
| **Location** | `file.js:LINE` or `file.js:LINEโLINE`. Two locations when the finding is about a relationship (duplication, a helper that exists but is bypassed). `file.js` (absent) when the finding is that something is missing. |
|
|
190
|
+
| **Finding** | Lead with a **bold claim in a few words**, then the evidence, then the consequence, then the repair. |
|
|
191
|
+
|
|
192
|
+
Order rows within a table by severity, ๐ด first.
|
|
193
|
+
|
|
194
|
+
The Rule column is what makes this a review against *this repo's stated architecture* rather
|
|
195
|
+
than generic advice. If a finding cannot be tied to a rule and is not a security or correctness
|
|
196
|
+
issue, it is probably a preference โ cut it.
|
|
197
|
+
|
|
198
|
+
### What the Location column asserts
|
|
199
|
+
|
|
200
|
+
**Every line in a ๐ด/๐ก Location is part of the defect.** The column is not "lines relevant to
|
|
201
|
+
this row" โ a reader treats each entry as something you are asking them to change, and a line
|
|
202
|
+
folded in for context arrives as an accusation against code that is fine. Contributing context
|
|
203
|
+
goes in the finding body, where it can be labelled as context.
|
|
204
|
+
|
|
205
|
+
One review put `app.js:20` โ `app.set('trust proxy', 1)` โ into the Location of a ๐ก about
|
|
206
|
+
rate-limiter keying. The trust-proxy line was background; the defect was elsewhere. The row read
|
|
207
|
+
as "change your trust proxy setting", which was not the finding and was not correct.
|
|
208
|
+
|
|
209
|
+
**Every path in a Location is under the reviewed source tree.** Two things reliably tempt their
|
|
210
|
+
way into a finding row and do not belong there:
|
|
211
|
+
|
|
212
|
+
- observations about a *previous review document* ("`ROUTE_REVIEW.md` cites the wrong line")
|
|
213
|
+
- observations about the *tooling* ("`sweep.py` reports this export as used, but that is a false
|
|
214
|
+
positive")
|
|
215
|
+
|
|
216
|
+
Both can be worth saying. Neither is a finding about the backend, and a reader scanning for code
|
|
217
|
+
to fix has to stop and work out that this row is not about code. Put them in a `## Notes on
|
|
218
|
+
inputs` section below the tables.
|
|
219
|
+
|
|
220
|
+
**A row claiming an absence cites the whole construct.** "No `return`", "never calls the
|
|
221
|
+
validator", "lacks a guard" โ you can only see an absence across a complete unit. A range that
|
|
222
|
+
stops before the closing brace does not support the claim, because the thing you say is missing
|
|
223
|
+
may be on the next line, and that is precisely how one review reported a function as missing its
|
|
224
|
+
`return results;` when the return was one line past the cited range. Cite from the signature to
|
|
225
|
+
the closing brace, and quote the closing lines or the command that establishes the absence.
|
|
226
|
+
`scripts/verify_citations.py` flags these; run it before the report is done.
|
|
227
|
+
|
|
228
|
+
### Rows whose claim names a set
|
|
229
|
+
|
|
230
|
+
Some findings are about a set rather than a spot: the keys a masking list is missing, the sites a
|
|
231
|
+
duplicated block appears at, the variables a config module reads unguarded, the endpoints without
|
|
232
|
+
a limiter. These are the rows that go wrong most often, and they go wrong the same way every
|
|
233
|
+
time โ the set is right minus one. A reader acts on the row, fixes what it names, and the missing
|
|
234
|
+
element survives with the row marked done. That is worse than not raising it, because now nobody
|
|
235
|
+
will look again.
|
|
236
|
+
|
|
237
|
+
So a row whose claim names a set carries three things:
|
|
238
|
+
|
|
239
|
+
1. **The count, stated as "N of N"** โ "three of the three request keys that reach the logger",
|
|
240
|
+
not "keys including `code` and `state`". Writing the count forces you to have one.
|
|
241
|
+
2. **Where the set came from**, named in the row: the sweep section, the grep, the inventory
|
|
242
|
+
column. "From `sweep.py --section request-inputs`" tells the reader the enumeration was
|
|
243
|
+
mechanical, and tells you that it was.
|
|
244
|
+
3. **One repair clause per element.** If the claim names three keys, the repair names three keys.
|
|
245
|
+
|
|
246
|
+
A repair with fewer clauses than the claim has elements is an **incomplete finding**, not a
|
|
247
|
+
finding with a shortened repair โ the missing clause is the whole cost of the row. This is
|
|
248
|
+
checkable by counting, which is why it is stated this way: "repair the whole finding" is good
|
|
249
|
+
advice that people follow when they remember the third element and not when they don't.
|
|
250
|
+
|
|
251
|
+
The corollary is that these enumerations come from the scripts, not from recall. Recall is what
|
|
252
|
+
produces "Nโ1 of N".
|
|
253
|
+
|
|
254
|
+
### Two things every repair does
|
|
255
|
+
|
|
256
|
+
**Name what the repair removes, not only what it adds.** A finding says one `JWT_SECRET` signs
|
|
257
|
+
both session tokens and image-proxy tokens; the repair proposes a shared `getJwtSecret()` helper
|
|
258
|
+
imported at all four sites. That is tidier code and the exact same defect โ one secret, two token
|
|
259
|
+
types โ now with a helper making it convenient. A repair that leaves the complained-of property
|
|
260
|
+
in place is not a repair. Say what stops being true: "image tokens move to `IMAGE_TOKEN_SECRET`,
|
|
261
|
+
so a leaked session key no longer mints image URLs."
|
|
262
|
+
|
|
263
|
+
**A repair that relocates code quotes the rule that gives the destination its job.** "Move this to
|
|
264
|
+
the service layer" is a guess unless the rules file says the service owns it, and the guess is
|
|
265
|
+
usually the layer you thought of first. Open the rules file, find the section for the destination,
|
|
266
|
+
and quote the sentence. One review moved file-type validation โ a `415` thrown on an unexpected
|
|
267
|
+
extension โ into a service, when the rules file had a `validators` section describing exactly that
|
|
268
|
+
responsibility; the section was never opened. Quoting it costs one line and makes the repair
|
|
269
|
+
arguable on the repo's own terms instead of on taste.
|
|
270
|
+
|
|
271
|
+
Beyond those two: every ๐ด and ๐ก row ends in a repair โ the function to extract, the module to
|
|
272
|
+
create, the export to narrow, the signature to change. The reader is deciding what to do on
|
|
273
|
+
Monday, and a row that stops at the consequence makes them re-derive a conclusion you already
|
|
274
|
+
reached. If the right repair is genuinely contested, name the options and their costs and say
|
|
275
|
+
which one the rules file favours; that still tells them where to start.
|
|
276
|
+
|
|
277
|
+
---
|
|
278
|
+
|
|
279
|
+
## Worked example rows
|
|
280
|
+
|
|
281
|
+
The specificity below is the target. Each names a location, states what is true there, says what
|
|
282
|
+
it costs, and says what to do โ a reader can act on any of them without asking a follow-up
|
|
283
|
+
question.
|
|
284
|
+
|
|
285
|
+
| Sev | Rule | Location | Finding |
|
|
286
|
+
|---|---|---|---|
|
|
287
|
+
| ๐ด | services | `auth.service.js:76` | **One global secret resets any account.** `secretCode !== process.env.FORGOT_PASSWORD_SECRET_CODE` has no per-user binding, no expiry, no single-use consumption. Anyone who learns it resets every user's password given only an email. Issue a per-user random token instead, stored hashed with an expiry and cleared on use. |
|
|
288
|
+
| ๐ก | services (DONTS) | `quotation.service.js:112, 131, 204, 288` | **Six helpers are exported but only ever called in-file.** `toNonNegativeNumber`, `round2`, `recalcItem`, `sanitizeDescriptionStyle`, `QUOTATION_ITEM_FIELDS`, `QUOTATION_HEADER_FIELDS` have no importer anywhere in `src`, `tests` or `scripts`, yet each has four or five callers inside this module. They are over-exported, not dead: drop them from the `export` block at `:640` and keep every definition. |
|
|
289
|
+
| ๐ด | controllers | `email.controller.js:89` + `:107` | **Two Graph round-trips for one request.** `getAttachmentMetadata` calls `microsoft.getMessage` via `withGraph` (`email.service.js:612`), then `getAttachmentBuffer` runs `withGraph` again (597) โ second connection load, second potential token refresh, second call for the same message. Should be one service function returning both. |
|
|
290
|
+
| ๐ก | services / config | `s3Image.service.js:318, 471` vs `auth.middleware.js:20` | **`JWT_SECRET` is reused for two different token types.** Confusion is blocked today only by payload-shape luck โ the auth middleware requires `payload.userId` (image tokens lack it) and the image verifier requires `payload.quotationId` (session tokens lack it). Neither check is documented as a security boundary, so a future payload change silently breaks it. `oauthState.js:3` already uses a dedicated secret โ follow that. |
|
|
291
|
+
| โ
| services | `s3Image.service.js:470โ495` | **Path traversal is properly closed.** The reconstructed key (488) is compared for equality against the signed `payload.key`, and that key was minted through a `^quotations\/([^/]+)\/images\/([^/]+)$` regex (298). A `..` in `:filename` cannot match a signed key. |
|
|
292
|
+
|
|
293
|
+
Note what the last row does: it reports a security control that a reviewer would naturally probe,
|
|
294
|
+
and records the specific attack it followed and why that attack fails. That is more useful than
|
|
295
|
+
silence, because the next reader does not have to re-derive it โ and it is a `โ
` about a failure
|
|
296
|
+
path, not about the code reading well.
|
|
297
|
+
|
|
298
|
+
Note what the fourth row does โ it names an adjacent place in the same codebase that already
|
|
299
|
+
solves the problem correctly. A fix the reader can copy from their own repo lands better than an
|
|
300
|
+
abstract recommendation.
|
|
301
|
+
|
|
302
|
+
Note what the second row does *not* do: it does not say "delete them". Six exports with no
|
|
303
|
+
importer look dead from outside the file and are not, and a review that says "safe to delete"
|
|
304
|
+
about identifiers with in-file callers hands the reader a change that breaks the module. Naming
|
|
305
|
+
the export block as the thing to edit is what makes the row safe to act on.
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
## Inventory tables are transcribed, not typed
|
|
310
|
+
|
|
311
|
+
The route inventory, and any per-file handler inventory, take their line numbers from
|
|
312
|
+
`scripts/list_routes.py` output โ paste the column, do not retype it from the file you have open.
|
|
313
|
+
|
|
314
|
+
This sounds pedantic and is not. Inventory rows are where citation errors cluster, because a
|
|
315
|
+
finding's citation gets checked while writing the argument around it and an inventory row gets
|
|
316
|
+
checked by nobody. One review's handler inventory for a single controller was off by three to six
|
|
317
|
+
lines on every row, in a document whose finding citations were almost all exact. A reader who
|
|
318
|
+
opens the first inventory row, lands three lines away, and finds nothing there has no way to know
|
|
319
|
+
the rest of the report is better than this.
|
|
320
|
+
|
|
321
|
+
---
|
|
322
|
+
|
|
323
|
+
## Closing sections
|
|
324
|
+
|
|
325
|
+
**Highest-value fixes, in order.** The tables are organised for lookup, which means they are not
|
|
326
|
+
organised for action โ a ๐ด in the last table is no less urgent than one in the first. This
|
|
327
|
+
section is the answer to "what do I do Monday". Rank by exploitability and blast radius rather
|
|
328
|
+
than by severity marker alone: an unauthenticated remote issue outranks an internal layering
|
|
329
|
+
violation even when both are ๐ด.
|
|
330
|
+
|
|
331
|
+
**Files read for this review.** State the blast radius. A reader who knows a service was never
|
|
332
|
+
opened can weigh the review's silence about it correctly. List what was referenced but not read
|
|
333
|
+
separately.
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Enumerate Express routes with their real mounted paths.
|
|
3
|
+
|
|
4
|
+
A route's real path is the router's mount prefix plus the path written in the
|
|
5
|
+
route file. Doc comments naming the path drift from the mount point and cannot
|
|
6
|
+
be trusted, so this resolves the prefix from the app entrypoint instead.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
python3 list_routes.py <src-dir> [--json]
|
|
10
|
+
|
|
11
|
+
Output is a scaffold, not a finding. Regex parsing misses dynamic mounts,
|
|
12
|
+
re-exported routers, and conditionally registered routes, so confirm every row
|
|
13
|
+
against the file before it goes into a review.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import json
|
|
18
|
+
import re
|
|
19
|
+
import sys
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
METHODS = ("get", "post", "put", "patch", "delete", "options", "head", "all")
|
|
23
|
+
|
|
24
|
+
ENTRYPOINT_NAMES = ("app.js", "server.js", "index.js", "app.ts", "server.ts", "index.ts")
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def find_call(text, start):
|
|
28
|
+
"""Return (inner_text, end_index) for the parenthesised call starting at `start`.
|
|
29
|
+
|
|
30
|
+
`start` is the index of the opening paren. Tracks string literals so a paren
|
|
31
|
+
or quote inside a route path doesn't unbalance the scan.
|
|
32
|
+
"""
|
|
33
|
+
depth = 0
|
|
34
|
+
i = start
|
|
35
|
+
quote = None
|
|
36
|
+
while i < len(text):
|
|
37
|
+
ch = text[i]
|
|
38
|
+
if quote:
|
|
39
|
+
if ch == "\\":
|
|
40
|
+
i += 2
|
|
41
|
+
continue
|
|
42
|
+
if ch == quote:
|
|
43
|
+
quote = None
|
|
44
|
+
elif ch in "'\"`":
|
|
45
|
+
quote = ch
|
|
46
|
+
elif ch == "(":
|
|
47
|
+
depth += 1
|
|
48
|
+
elif ch == ")":
|
|
49
|
+
depth -= 1
|
|
50
|
+
if depth == 0:
|
|
51
|
+
return text[start + 1 : i], i
|
|
52
|
+
i += 1
|
|
53
|
+
return None, len(text)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def split_args(inner):
|
|
57
|
+
"""Split a call's argument list on top-level commas."""
|
|
58
|
+
args, depth, quote, current = [], 0, None, []
|
|
59
|
+
for i, ch in enumerate(inner):
|
|
60
|
+
if quote:
|
|
61
|
+
current.append(ch)
|
|
62
|
+
if ch == "\\":
|
|
63
|
+
continue
|
|
64
|
+
if ch == quote:
|
|
65
|
+
quote = None
|
|
66
|
+
continue
|
|
67
|
+
if ch in "'\"`":
|
|
68
|
+
quote = ch
|
|
69
|
+
current.append(ch)
|
|
70
|
+
continue
|
|
71
|
+
if ch in "([{":
|
|
72
|
+
depth += 1
|
|
73
|
+
elif ch in ")]}":
|
|
74
|
+
depth -= 1
|
|
75
|
+
if ch == "," and depth == 0:
|
|
76
|
+
args.append("".join(current).strip())
|
|
77
|
+
current = []
|
|
78
|
+
continue
|
|
79
|
+
current.append(ch)
|
|
80
|
+
if current:
|
|
81
|
+
args.append("".join(current).strip())
|
|
82
|
+
return [a for a in args if a]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def line_of(text, index):
|
|
86
|
+
return text.count("\n", 0, index) + 1
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def strip_quotes(arg):
|
|
90
|
+
arg = arg.strip()
|
|
91
|
+
if len(arg) >= 2 and arg[0] in "'\"`" and arg[-1] == arg[0]:
|
|
92
|
+
return arg[1:-1]
|
|
93
|
+
return None
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def clean_ident(arg):
|
|
97
|
+
"""Reduce an argument expression to something readable in a table cell."""
|
|
98
|
+
arg = " ".join(arg.split())
|
|
99
|
+
if len(arg) > 40:
|
|
100
|
+
arg = arg[:37] + "..."
|
|
101
|
+
return arg
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def parse_imports(text):
|
|
105
|
+
"""Map imported identifier -> resolved module path fragment."""
|
|
106
|
+
out = {}
|
|
107
|
+
for m in re.finditer(r"import\s+(\w+)\s+from\s+['\"]([^'\"]+)['\"]", text):
|
|
108
|
+
out[m.group(1)] = m.group(2)
|
|
109
|
+
return out
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def parse_mounts(entry_text, imports):
|
|
113
|
+
"""Map router identifier -> mount prefix, and collect non-router app.use middleware.
|
|
114
|
+
|
|
115
|
+
Returns (mounts, prefix_middleware) where mounts is {identifier: prefix} and
|
|
116
|
+
prefix_middleware is a list of (prefix, identifier, line) for middleware applied
|
|
117
|
+
by path prefix rather than inside a route file -- rate limiters, most often.
|
|
118
|
+
Those are worth surfacing because they are invisible from the route file.
|
|
119
|
+
"""
|
|
120
|
+
mounts, prefix_middleware = {}, []
|
|
121
|
+
for m in re.finditer(r"\bapp\.use\s*\(", entry_text):
|
|
122
|
+
inner, _ = find_call(entry_text, m.end() - 1)
|
|
123
|
+
if inner is None:
|
|
124
|
+
continue
|
|
125
|
+
args = split_args(inner)
|
|
126
|
+
if len(args) < 2:
|
|
127
|
+
continue
|
|
128
|
+
prefix = strip_quotes(args[0])
|
|
129
|
+
if prefix is None:
|
|
130
|
+
continue
|
|
131
|
+
for ident in args[1:]:
|
|
132
|
+
ident = ident.strip()
|
|
133
|
+
if not re.fullmatch(r"\w+", ident):
|
|
134
|
+
continue
|
|
135
|
+
source = imports.get(ident, "")
|
|
136
|
+
if "route" in source.lower() or "route" in ident.lower():
|
|
137
|
+
mounts[ident] = prefix
|
|
138
|
+
else:
|
|
139
|
+
prefix_middleware.append(
|
|
140
|
+
(prefix, ident, line_of(entry_text, m.start()))
|
|
141
|
+
)
|
|
142
|
+
return mounts, prefix_middleware
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def parse_routes(text, filename, prefix):
|
|
146
|
+
"""Extract route definitions from a router file."""
|
|
147
|
+
rows = []
|
|
148
|
+
pattern = re.compile(r"\b(?:router|app)\.(" + "|".join(METHODS) + r")\s*\(")
|
|
149
|
+
for m in pattern.finditer(text):
|
|
150
|
+
method = m.group(1).upper()
|
|
151
|
+
inner, _ = find_call(text, m.end() - 1)
|
|
152
|
+
if inner is None:
|
|
153
|
+
continue
|
|
154
|
+
args = split_args(inner)
|
|
155
|
+
if not args:
|
|
156
|
+
continue
|
|
157
|
+
route_path = strip_quotes(args[0])
|
|
158
|
+
if route_path is None:
|
|
159
|
+
continue
|
|
160
|
+
rest = args[1:]
|
|
161
|
+
handler = clean_ident(rest[-1]) if rest else ""
|
|
162
|
+
middleware = [clean_ident(a) for a in rest[:-1]] if len(rest) > 1 else []
|
|
163
|
+
full = (prefix.rstrip("/") + route_path) if prefix else route_path
|
|
164
|
+
if not full.startswith("/"):
|
|
165
|
+
full = "/" + full
|
|
166
|
+
rows.append(
|
|
167
|
+
{
|
|
168
|
+
"method": method,
|
|
169
|
+
"path": full,
|
|
170
|
+
"route_path": route_path,
|
|
171
|
+
"file": filename,
|
|
172
|
+
"line": line_of(text, m.start()),
|
|
173
|
+
"middleware": middleware,
|
|
174
|
+
"handler": handler,
|
|
175
|
+
"has_auth": any("auth" in x.lower() for x in middleware),
|
|
176
|
+
"has_validator": any(
|
|
177
|
+
"valid" in x.lower() for x in middleware
|
|
178
|
+
),
|
|
179
|
+
}
|
|
180
|
+
)
|
|
181
|
+
return rows
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def main():
|
|
185
|
+
ap = argparse.ArgumentParser(description=__doc__)
|
|
186
|
+
ap.add_argument("src", help="backend source directory (the one containing app.js and routes/)")
|
|
187
|
+
ap.add_argument("--json", action="store_true", help="emit JSON instead of markdown")
|
|
188
|
+
args = ap.parse_args()
|
|
189
|
+
|
|
190
|
+
src = Path(args.src)
|
|
191
|
+
if not src.is_dir():
|
|
192
|
+
sys.exit(f"not a directory: {src}")
|
|
193
|
+
|
|
194
|
+
entry = next((src / n for n in ENTRYPOINT_NAMES if (src / n).exists()), None)
|
|
195
|
+
if entry is None:
|
|
196
|
+
sys.exit(f"no app entrypoint found in {src} (looked for {', '.join(ENTRYPOINT_NAMES)})")
|
|
197
|
+
|
|
198
|
+
entry_text = entry.read_text(encoding="utf-8", errors="replace")
|
|
199
|
+
imports = parse_imports(entry_text)
|
|
200
|
+
mounts, prefix_middleware = parse_mounts(entry_text, imports)
|
|
201
|
+
|
|
202
|
+
rows = []
|
|
203
|
+
|
|
204
|
+
# Routes defined inline in the entrypoint. These are worth flagging on sight:
|
|
205
|
+
# an endpoint outside routes/ has no controller and usually no middleware.
|
|
206
|
+
for r in parse_routes(entry_text, entry.name, ""):
|
|
207
|
+
r["inline_in_entrypoint"] = True
|
|
208
|
+
rows.append(r)
|
|
209
|
+
|
|
210
|
+
routes_dir = src / "routes"
|
|
211
|
+
if routes_dir.is_dir():
|
|
212
|
+
by_source = {}
|
|
213
|
+
for ident, prefix in mounts.items():
|
|
214
|
+
source = imports.get(ident, "")
|
|
215
|
+
by_source[Path(source).name] = prefix
|
|
216
|
+
|
|
217
|
+
for f in sorted(routes_dir.iterdir()):
|
|
218
|
+
if f.suffix not in (".js", ".ts", ".mjs"):
|
|
219
|
+
continue
|
|
220
|
+
prefix = by_source.get(f.name)
|
|
221
|
+
if prefix is None:
|
|
222
|
+
prefix = ""
|
|
223
|
+
text = f.read_text(encoding="utf-8", errors="replace")
|
|
224
|
+
for r in parse_routes(text, f.name, prefix):
|
|
225
|
+
r["inline_in_entrypoint"] = False
|
|
226
|
+
r["mounted"] = f.name in by_source
|
|
227
|
+
rows.append(r)
|
|
228
|
+
|
|
229
|
+
if args.json:
|
|
230
|
+
print(json.dumps({"routes": rows, "prefix_middleware": prefix_middleware}, indent=2))
|
|
231
|
+
return
|
|
232
|
+
|
|
233
|
+
print(f"# Route inventory โ {src}\n")
|
|
234
|
+
print(f"Entrypoint: `{entry.name}` ยท {len(rows)} routes\n")
|
|
235
|
+
print("| # | Method | Path (as mounted) | File:line | Middleware | Handler | Auth | Validator |")
|
|
236
|
+
print("|---|---|---|---|---|---|---|---|")
|
|
237
|
+
for i, r in enumerate(rows, 1):
|
|
238
|
+
mw = ", ".join(f"`{x}`" for x in r["middleware"]) or "โ"
|
|
239
|
+
note = " **(inline)**" if r["inline_in_entrypoint"] else ""
|
|
240
|
+
print(
|
|
241
|
+
f"| {i} | {r['method']} | `{r['path']}`{note} | `{r['file']}:{r['line']}` "
|
|
242
|
+
f"| {mw} | `{r['handler']}` | {'yes' if r['has_auth'] else 'โ'} "
|
|
243
|
+
f"| {'yes' if r['has_validator'] else 'โ'} |"
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
with_validator = sum(1 for r in rows if r["has_validator"])
|
|
247
|
+
print(f"\n**{with_validator} of {len(rows)} routes have a validator.**")
|
|
248
|
+
|
|
249
|
+
unmounted = sorted({r["file"] for r in rows if not r["inline_in_entrypoint"] and not r.get("mounted")})
|
|
250
|
+
if unmounted:
|
|
251
|
+
print(
|
|
252
|
+
f"\n> Could not resolve a mount prefix for: {', '.join(f'`{u}`' for u in unmounted)}. "
|
|
253
|
+
"Paths above are unprefixed โ resolve by hand before using."
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
if prefix_middleware:
|
|
257
|
+
print("\n## Middleware applied by path prefix\n")
|
|
258
|
+
print(
|
|
259
|
+
"Applied in the entrypoint rather than in a route file, so it is invisible "
|
|
260
|
+
"when reading the route. `app.use` matches all methods and all subpaths.\n"
|
|
261
|
+
)
|
|
262
|
+
print("| Prefix | Middleware | Entrypoint line |")
|
|
263
|
+
print("|---|---|---|")
|
|
264
|
+
for prefix, ident, line in prefix_middleware:
|
|
265
|
+
print(f"| `{prefix}` | `{ident}` | {line} |")
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
if __name__ == "__main__":
|
|
269
|
+
main()
|