@shriyanss/js-recon 1.4.1-alpha.4 → 1.4.1-alpha.5

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 (189) hide show
  1. package/.github/workflows/build-and-prettify.yaml +187 -1
  2. package/.github/workflows/publish-js-recon.yml +0 -1
  3. package/.github/workflows/rules-smoke-test.yaml +79 -0
  4. package/CHANGELOG.md +40 -0
  5. package/CLAUDE.md +184 -8
  6. package/README.md +18 -17
  7. package/build/analyze/engine/csMastSEngine.js +129 -0
  8. package/build/analyze/engine/csMastSEngine.js.map +1 -0
  9. package/build/analyze/engine/index.js +15 -0
  10. package/build/analyze/engine/index.js.map +1 -1
  11. package/build/analyze/helpers/engineHelpers/findDirectAssignment.js +2 -1
  12. package/build/analyze/helpers/engineHelpers/findDirectAssignment.js.map +1 -1
  13. package/build/analyze/helpers/engineHelpers/findMemberExpressionAssignment.js +2 -1
  14. package/build/analyze/helpers/engineHelpers/findMemberExpressionAssignment.js.map +1 -1
  15. package/build/analyze/helpers/engineHelpers/resolveFunctionIdentifier.js +2 -1
  16. package/build/analyze/helpers/engineHelpers/resolveFunctionIdentifier.js.map +1 -1
  17. package/build/analyze/helpers/engineHelpers/taintFlow.js +11 -2
  18. package/build/analyze/helpers/engineHelpers/taintFlow.js.map +1 -1
  19. package/build/analyze/helpers/schemas.js +5 -1
  20. package/build/analyze/helpers/schemas.js.map +1 -1
  21. package/build/analyze/helpers/validate.js +3 -3
  22. package/build/analyze/helpers/validate.js.map +1 -1
  23. package/build/completion/index.js +432 -0
  24. package/build/completion/index.js.map +1 -0
  25. package/build/cs_mast/index.js +94 -3
  26. package/build/cs_mast/index.js.map +1 -1
  27. package/build/endpoints/next_js/client_jsFilesHref.js +2 -1
  28. package/build/endpoints/next_js/client_jsFilesHref.js.map +1 -1
  29. package/build/endpoints/next_js/client_jsonParse.js +2 -1
  30. package/build/endpoints/next_js/client_jsonParse.js.map +1 -1
  31. package/build/endpoints/next_js/client_mappedJsonFile.js +2 -1
  32. package/build/endpoints/next_js/client_mappedJsonFile.js.map +1 -1
  33. package/build/endpoints/next_js/client_subsequentRequests.js +2 -1
  34. package/build/endpoints/next_js/client_subsequentRequests.js.map +1 -1
  35. package/build/fingerprint/index.js +56 -33
  36. package/build/fingerprint/index.js.map +1 -1
  37. package/build/globalConfig.js +1 -1
  38. package/build/index.js +67 -6
  39. package/build/index.js.map +1 -1
  40. package/build/lazyLoad/angular/angular_getFromMainJs.js +2 -1
  41. package/build/lazyLoad/angular/angular_getFromMainJs.js.map +1 -1
  42. package/build/lazyLoad/downloadFilesUtil.js +6 -5
  43. package/build/lazyLoad/downloadFilesUtil.js.map +1 -1
  44. package/build/lazyLoad/downloadLoadedJsUtil.js +56 -7
  45. package/build/lazyLoad/downloadLoadedJsUtil.js.map +1 -1
  46. package/build/lazyLoad/downloadQueue.js +9 -8
  47. package/build/lazyLoad/downloadQueue.js.map +1 -1
  48. package/build/lazyLoad/index.js +28 -1
  49. package/build/lazyLoad/index.js.map +1 -1
  50. package/build/lazyLoad/next_js/next_GetLazyResourcesBuildManifestJs.js +36 -34
  51. package/build/lazyLoad/next_js/next_GetLazyResourcesBuildManifestJs.js.map +1 -1
  52. package/build/lazyLoad/next_js/next_GetLazyResourcesWebpackJs.js +63 -15
  53. package/build/lazyLoad/next_js/next_GetLazyResourcesWebpackJs.js.map +1 -1
  54. package/build/lazyLoad/next_js/next_parseLayoutJs.js +80 -68
  55. package/build/lazyLoad/next_js/next_parseLayoutJs.js.map +1 -1
  56. package/build/lazyLoad/next_js/next_promiseResolve.js +22 -16
  57. package/build/lazyLoad/next_js/next_promiseResolve.js.map +1 -1
  58. package/build/lazyLoad/nuxt_js/nuxt_astParse.js +49 -47
  59. package/build/lazyLoad/nuxt_js/nuxt_astParse.js.map +1 -1
  60. package/build/lazyLoad/nuxt_js/nuxt_getFromPageSource.js +3 -1
  61. package/build/lazyLoad/nuxt_js/nuxt_getFromPageSource.js.map +1 -1
  62. package/build/lazyLoad/nuxt_js/nuxt_stringAnalysisJSFiles.js +3 -2
  63. package/build/lazyLoad/nuxt_js/nuxt_stringAnalysisJSFiles.js.map +1 -1
  64. package/build/lazyLoad/react/react_followImports.js +12 -9
  65. package/build/lazyLoad/react/react_followImports.js.map +1 -1
  66. package/build/lazyLoad/react/react_webpackChunkPaths.js +175 -123
  67. package/build/lazyLoad/react/react_webpackChunkPaths.js.map +1 -1
  68. package/build/lazyLoad/svelte/svelte_stringAnalysisJSFiles.js +3 -2
  69. package/build/lazyLoad/svelte/svelte_stringAnalysisJSFiles.js.map +1 -1
  70. package/build/lazyLoad/techDetect/checkAngularJS.js +27 -6
  71. package/build/lazyLoad/techDetect/checkAngularJS.js.map +1 -1
  72. package/build/lazyLoad/techDetect/checkReact.js +8 -0
  73. package/build/lazyLoad/techDetect/checkReact.js.map +1 -1
  74. package/build/lazyLoad/techDetect/index.js +23 -2
  75. package/build/lazyLoad/techDetect/index.js.map +1 -1
  76. package/build/lazyLoad/vue/vue_RuntimeJs.js +2 -1
  77. package/build/lazyLoad/vue/vue_RuntimeJs.js.map +1 -1
  78. package/build/lazyLoad/vue/vue_SingleJsFileOnHome.js +2 -1
  79. package/build/lazyLoad/vue/vue_SingleJsFileOnHome.js.map +1 -1
  80. package/build/lazyLoad/vue/vue_discoverJsFiles.js +22 -10
  81. package/build/lazyLoad/vue/vue_discoverJsFiles.js.map +1 -1
  82. package/build/lazyLoad/vue/vue_getClientSidePaths.js +2 -1
  83. package/build/lazyLoad/vue/vue_getClientSidePaths.js.map +1 -1
  84. package/build/lazyLoad/vue/vue_jsImports.js +2 -1
  85. package/build/lazyLoad/vue/vue_jsImports.js.map +1 -1
  86. package/build/lazyLoad/vue/vue_viteMapDeps.js +62 -54
  87. package/build/lazyLoad/vue/vue_viteMapDeps.js.map +1 -1
  88. package/build/map/angular_js/getAngularConnections.js +1 -1
  89. package/build/map/angular_js/getAngularConnections.js.map +1 -1
  90. package/build/map/graphql/resolveGraphql.js +2 -1
  91. package/build/map/graphql/resolveGraphql.js.map +1 -1
  92. package/build/map/next_js/getExports.js +2 -1
  93. package/build/map/next_js/getExports.js.map +1 -1
  94. package/build/map/next_js/getFetchInstances.js +2 -1
  95. package/build/map/next_js/getFetchInstances.js.map +1 -1
  96. package/build/map/next_js/getTurbopackConnections.js +2 -1
  97. package/build/map/next_js/getTurbopackConnections.js.map +1 -1
  98. package/build/map/next_js/getWebpackConnections.js +2 -1
  99. package/build/map/next_js/getWebpackConnections.js.map +1 -1
  100. package/build/map/next_js/resolveAxios.js +2 -1
  101. package/build/map/next_js/resolveAxios.js.map +1 -1
  102. package/build/map/next_js/resolveAxiosHelpers/directCallsWithoutAssignment.js +2 -1
  103. package/build/map/next_js/resolveAxiosHelpers/directCallsWithoutAssignment.js.map +1 -1
  104. package/build/map/next_js/resolveAxiosHelpers/findAxiosInstance.js +2 -1
  105. package/build/map/next_js/resolveAxiosHelpers/findAxiosInstance.js.map +1 -1
  106. package/build/map/next_js/resolveAxiosHelpers/findCrossChunkParams.js +2 -1
  107. package/build/map/next_js/resolveAxiosHelpers/findCrossChunkParams.js.map +1 -1
  108. package/build/map/next_js/resolveAxiosHelpers/handleAxiosCreate.js +2 -1
  109. package/build/map/next_js/resolveAxiosHelpers/handleAxiosCreate.js.map +1 -1
  110. package/build/map/next_js/resolveAxiosHelpers/handleZDotCreate.js +2 -1
  111. package/build/map/next_js/resolveAxiosHelpers/handleZDotCreate.js.map +1 -1
  112. package/build/map/next_js/resolveAxiosHelpers/interceptorHeaders.js +2 -1
  113. package/build/map/next_js/resolveAxiosHelpers/interceptorHeaders.js.map +1 -1
  114. package/build/map/next_js/resolveAxiosHelpers/processExportedEndpoints.js +2 -1
  115. package/build/map/next_js/resolveAxiosHelpers/processExportedEndpoints.js.map +1 -1
  116. package/build/map/next_js/resolveAxiosHelpers/traceAxiosInstanceExports.js +2 -1
  117. package/build/map/next_js/resolveAxiosHelpers/traceAxiosInstanceExports.js.map +1 -1
  118. package/build/map/next_js/resolveAxiosHelpers/traceBody.js +2 -1
  119. package/build/map/next_js/resolveAxiosHelpers/traceBody.js.map +1 -1
  120. package/build/map/next_js/resolveFetch.js +2 -1
  121. package/build/map/next_js/resolveFetch.js.map +1 -1
  122. package/build/map/next_js/resolveNewRequest.js +2 -1
  123. package/build/map/next_js/resolveNewRequest.js.map +1 -1
  124. package/build/map/next_js/resolveServerActions.js +2 -1
  125. package/build/map/next_js/resolveServerActions.js.map +1 -1
  126. package/build/map/next_js/utils.js +2 -1
  127. package/build/map/next_js/utils.js.map +1 -1
  128. package/build/map/react_js/getReactConnections.js +5 -4
  129. package/build/map/react_js/getReactConnections.js.map +1 -1
  130. package/build/map/vue_js/crossFileResolver.js +3 -2
  131. package/build/map/vue_js/crossFileResolver.js.map +1 -1
  132. package/build/map/vue_js/getViteConnections.js +5 -4
  133. package/build/map/vue_js/getViteConnections.js.map +1 -1
  134. package/build/map/vue_js/taint_utils.js +2 -1
  135. package/build/map/vue_js/taint_utils.js.map +1 -1
  136. package/build/map/vue_js/vue_resolveFetch.js +3 -2
  137. package/build/map/vue_js/vue_resolveFetch.js.map +1 -1
  138. package/build/map/vue_js/vue_resolveHttpClient.js +3 -2
  139. package/build/map/vue_js/vue_resolveHttpClient.js.map +1 -1
  140. package/build/map/vue_js/vue_resolveXhr.js +3 -2
  141. package/build/map/vue_js/vue_resolveXhr.js.map +1 -1
  142. package/build/refactor/index.js +399 -60
  143. package/build/refactor/index.js.map +1 -1
  144. package/build/refactor/next/helpers.js +194 -13
  145. package/build/refactor/next/helpers.js.map +1 -1
  146. package/build/refactor/next/index.js +223 -36
  147. package/build/refactor/next/index.js.map +1 -1
  148. package/build/refactor/next/transform.js +905 -55
  149. package/build/refactor/next/transform.js.map +1 -1
  150. package/build/refactor/react/index.js +3 -3
  151. package/build/refactor/react/index.js.map +1 -1
  152. package/build/refactor/react/transform.js +2 -1
  153. package/build/refactor/react/transform.js.map +1 -1
  154. package/build/refactor/react/validator.js +2 -1
  155. package/build/refactor/react/validator.js.map +1 -1
  156. package/build/refactor/react-vite/index.js +39 -5
  157. package/build/refactor/react-vite/index.js.map +1 -1
  158. package/build/refactor/react-vite/vendor-analyze.js +3 -3
  159. package/build/refactor/react-vite/vendor-analyze.js.map +1 -1
  160. package/build/refactor/remote/config.js +17 -1
  161. package/build/refactor/remote/config.js.map +1 -1
  162. package/build/refactor/remote/hf-client.js +27 -2
  163. package/build/refactor/remote/hf-client.js.map +1 -1
  164. package/build/refactor/remote/version-detect.js +355 -0
  165. package/build/refactor/remote/version-detect.js.map +1 -0
  166. package/build/refactor/vue/index.js +176 -0
  167. package/build/refactor/vue/index.js.map +1 -0
  168. package/build/refactor/vue/vendor-analyze-vue.js +271 -0
  169. package/build/refactor/vue/vendor-analyze-vue.js.map +1 -0
  170. package/build/refactor/vue/vite.js +216 -0
  171. package/build/refactor/vue/vite.js.map +1 -0
  172. package/build/report/utility/genHtml.js +14 -3
  173. package/build/report/utility/genHtml.js.map +1 -1
  174. package/build/run/bundler-detect.js +140 -0
  175. package/build/run/bundler-detect.js.map +1 -0
  176. package/build/run/index.js +91 -5
  177. package/build/run/index.js.map +1 -1
  178. package/build/strings/index.js +6 -2
  179. package/build/strings/index.js.map +1 -1
  180. package/build/strings/trufflehog.js +71 -0
  181. package/build/strings/trufflehog.js.map +1 -0
  182. package/build/utility/makeReq.js +10 -9
  183. package/build/utility/makeReq.js.map +1 -1
  184. package/build/utility/postmanGenerator.js +2 -2
  185. package/build/utility/postmanGenerator.js.map +1 -1
  186. package/build/utility/puppeteerInstance.js +7 -1
  187. package/build/utility/puppeteerInstance.js.map +1 -1
  188. package/package.json +9 -5
  189. package/scripts/smoke-test.js +74 -0
@@ -89,9 +89,30 @@ jobs:
89
89
  git commit -m "chore: prettify code" || echo "No changes to commit"
90
90
  git push
91
91
 
92
- build:
92
+ test:
93
93
  needs: prettier
94
94
  runs-on: ubuntu-latest
95
+ steps:
96
+ - name: Checkout
97
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
98
+ with:
99
+ ref: ${{ github.ref_name }}
100
+
101
+ - name: Set up Node.js
102
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
103
+ with:
104
+ node-version: "22"
105
+ cache: "npm"
106
+
107
+ - name: Install dependencies
108
+ run: npm ci
109
+
110
+ - name: Run unit tests
111
+ run: npm test
112
+
113
+ build:
114
+ needs: test
115
+ runs-on: ubuntu-latest
95
116
  steps:
96
117
  - name: Checkout
97
118
  uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
@@ -119,3 +140,168 @@ jobs:
119
140
  ref: ${{ github.ref_name }}
120
141
  - name: Build docker container
121
142
  run: docker build -t shriyanss/js-recon .
143
+
144
+ integration-test-docker:
145
+ needs: build-docker
146
+ runs-on: ubuntu-latest
147
+ timeout-minutes: 30
148
+ steps:
149
+ - name: Checkout js-recon
150
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
151
+ with:
152
+ ref: ${{ github.ref_name }}
153
+
154
+ - name: Checkout js-recon-labs
155
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
156
+ with:
157
+ repository: shriyanss/js-recon-labs
158
+ path: js-recon-labs
159
+
160
+ - name: Checkout js-recon-rules
161
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
162
+ with:
163
+ repository: shriyanss/js-recon-rules
164
+ path: js-recon-rules
165
+
166
+ - name: Set up Node.js
167
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
168
+ with:
169
+ node-version: "22"
170
+ cache: "npm"
171
+
172
+ - name: Build js-recon Docker image
173
+ run: docker build -t shriyanss/js-recon .
174
+
175
+ - name: Install vuln-all-rules app dependencies
176
+ working-directory: js-recon-labs/next_js/vuln-all-rules
177
+ run: npm ci
178
+
179
+ - name: Build vuln-all-rules app
180
+ working-directory: js-recon-labs/next_js/vuln-all-rules
181
+ run: npm run build
182
+
183
+ - name: Start vuln-all-rules app
184
+ working-directory: js-recon-labs/next_js/vuln-all-rules
185
+ run: npm start &
186
+
187
+ - name: Wait for lab app to be ready
188
+ run: |
189
+ echo "Waiting for lab app on port 3001..."
190
+ n=0
191
+ until curl -sf http://localhost:3001 > /dev/null; do
192
+ n=$((n+1))
193
+ [ $n -ge 30 ] && echo "Timed out waiting for lab app" && exit 1
194
+ sleep 2
195
+ done
196
+ echo "Lab app is ready."
197
+
198
+ - name: Run js-recon via Docker against lab app
199
+ run: |
200
+ docker run --name js-recon-run \
201
+ --network host \
202
+ -v "$(pwd)/js-recon-rules:/home/pptruser/js-recon-rules" \
203
+ --entrypoint /bin/bash \
204
+ shriyanss/js-recon \
205
+ -c 'mkdir -p ~/.js-recon && ln -sf /home/pptruser/js-recon-rules ~/.js-recon/rules && printf "http://localhost:3001\n" > /tmp/urls.txt && node build/index.js run -u /tmp/urls.txt -r ./js-recon-rules --no-sandbox -y -k'
206
+ mkdir -p output
207
+ docker cp js-recon-run:/home/pptruser/output/. ./output/ 2>/dev/null || true
208
+ docker rm js-recon-run
209
+
210
+ - name: Verify all rules fired
211
+ run: node scripts/smoke-test.js
212
+
213
+ - name: Upload analyze.json on failure
214
+ if: failure()
215
+ uses: actions/upload-artifact@v4
216
+ with:
217
+ name: analyze-json-docker
218
+ path: output/localhost_3001/analyze.json
219
+ if-no-files-found: ignore
220
+
221
+ integration-test-macos:
222
+ needs: build
223
+ runs-on: macos-latest
224
+ timeout-minutes: 30
225
+ steps:
226
+ - name: Checkout js-recon
227
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
228
+ with:
229
+ ref: ${{ github.ref_name }}
230
+
231
+ - name: Checkout js-recon-labs
232
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
233
+ with:
234
+ repository: shriyanss/js-recon-labs
235
+ path: js-recon-labs
236
+
237
+ - name: Checkout js-recon-rules
238
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
239
+ with:
240
+ repository: shriyanss/js-recon-rules
241
+ path: js-recon-rules
242
+
243
+ - name: Set up Node.js
244
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
245
+ with:
246
+ node-version: "22"
247
+ cache: "npm"
248
+
249
+ - name: Install Chrome
250
+ run: brew install --cask google-chrome
251
+
252
+ - name: Install js-recon dependencies
253
+ run: npm ci
254
+ env:
255
+ PUPPETEER_SKIP_DOWNLOAD: "true"
256
+
257
+ - name: Build js-recon
258
+ run: npm run build
259
+
260
+ - name: Install vuln-all-rules app dependencies
261
+ working-directory: js-recon-labs/next_js/vuln-all-rules
262
+ run: npm ci
263
+
264
+ - name: Build vuln-all-rules app
265
+ working-directory: js-recon-labs/next_js/vuln-all-rules
266
+ run: npm run build
267
+
268
+ - name: Start vuln-all-rules app
269
+ working-directory: js-recon-labs/next_js/vuln-all-rules
270
+ run: npm start &
271
+
272
+ - name: Wait for lab app to be ready
273
+ run: |
274
+ echo "Waiting for lab app on port 3001..."
275
+ n=0
276
+ until curl -sf http://localhost:3001 > /dev/null; do
277
+ n=$((n+1))
278
+ [ $n -ge 30 ] && echo "Timed out waiting for lab app" && exit 1
279
+ sleep 2
280
+ done
281
+ echo "Lab app is ready."
282
+
283
+ - name: Pre-populate js-recon rules cache
284
+ run: |
285
+ mkdir -p ~/.js-recon
286
+ ln -sf "$GITHUB_WORKSPACE/js-recon-rules" ~/.js-recon/rules
287
+
288
+ - name: Run js-recon natively against lab app
289
+ run: |
290
+ echo "http://localhost:3001" > /tmp/smoke-urls.txt
291
+ node --max-old-space-size=4096 build/index.js run \
292
+ -u /tmp/smoke-urls.txt \
293
+ -r ./js-recon-rules \
294
+ -y -k
295
+ env:
296
+ PUPPETEER_EXECUTABLE_PATH: /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
297
+
298
+ - name: Verify all rules fired
299
+ run: node scripts/smoke-test.js
300
+
301
+ - name: Upload analyze.json on failure
302
+ if: failure()
303
+ uses: actions/upload-artifact@v4
304
+ with:
305
+ name: analyze-json-macos
306
+ path: output/localhost_3001/analyze.json
307
+ if-no-files-found: ignore
@@ -113,7 +113,6 @@ jobs:
113
113
  VERSION="${{ steps.version.outputs.version }}"
114
114
  SHA="${{ steps.sha256.outputs.sha256 }}"
115
115
  F="Formula/js-recon.rb"
116
- sed -i "s|^ version \".*\"| version \"${VERSION}\"|" "${F}"
117
116
  sed -i "s|^ url \"https://registry\.npmjs\.org/@shriyanss/js-recon/-/js-recon-[^\"]*\"| url \"https://registry.npmjs.org/@shriyanss/js-recon/-/js-recon-${VERSION}.tgz\"|" "${F}"
118
117
  sed -i "s|^ sha256 \"[a-f0-9]*\"| sha256 \"${SHA}\"|" "${F}"
119
118
 
@@ -0,0 +1,79 @@
1
+ name: Rules Smoke Test
2
+
3
+ on:
4
+ push:
5
+ branches-ignore:
6
+ - main
7
+
8
+ jobs:
9
+ rules-smoke-test:
10
+ runs-on: ubuntu-latest
11
+ timeout-minutes: 30
12
+
13
+ steps:
14
+ - name: Checkout js-recon
15
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
16
+
17
+ - name: Checkout js-recon-labs
18
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
19
+ with:
20
+ repository: shriyanss/js-recon-labs
21
+ path: js-recon-labs
22
+
23
+ - name: Checkout js-recon-rules
24
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
25
+ with:
26
+ repository: shriyanss/js-recon-rules
27
+ path: js-recon-rules
28
+
29
+ - name: Set up Node.js
30
+ uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
31
+ with:
32
+ node-version: "22"
33
+ cache: "npm"
34
+
35
+ - name: Install js-recon dependencies
36
+ run: npm ci
37
+
38
+ - name: Build js-recon
39
+ run: npm run build
40
+
41
+ - name: Install vulnerable app dependencies
42
+ working-directory: js-recon-labs/next_js/vuln-all-rules
43
+ run: npm ci
44
+
45
+ - name: Build vulnerable app
46
+ working-directory: js-recon-labs/next_js/vuln-all-rules
47
+ run: npm run build
48
+
49
+ - name: Start vulnerable app
50
+ working-directory: js-recon-labs/next_js/vuln-all-rules
51
+ run: npm start &
52
+
53
+ - name: Wait for app to be ready
54
+ run: |
55
+ echo "Waiting for app on port 3001..."
56
+ timeout 60 bash -c 'until curl -sf http://localhost:3001 > /dev/null; do sleep 2; done'
57
+ echo "App is ready."
58
+
59
+ - name: Run js-recon against vulnerable app
60
+ run: |
61
+ echo "http://localhost:3001" > /tmp/smoke-urls.txt
62
+ node --max-old-space-size=4096 build/index.js run \
63
+ -u /tmp/smoke-urls.txt \
64
+ -r ./js-recon-rules \
65
+ --no-sandbox \
66
+ -y -k
67
+ env:
68
+ PUPPETEER_EXECUTABLE_PATH: /usr/bin/google-chrome-stable
69
+
70
+ - name: Verify all rules fired
71
+ run: node scripts/smoke-test.js
72
+
73
+ - name: Upload analyze.json on failure
74
+ if: failure()
75
+ uses: actions/upload-artifact@v4
76
+ with:
77
+ name: analyze-json
78
+ path: output/localhost_3001/analyze.json
79
+ if-no-files-found: ignore
package/CHANGELOG.md CHANGED
@@ -1,5 +1,45 @@
1
1
  # Change Log
2
2
 
3
+ ## 1.4.1-alpha.5 - 2026-07-13
4
+
5
+ ### Fixed
6
+
7
+ - Homebrew install: `puppeteer-extra` now receives the ESM `puppeteer` instance directly via `addExtra()` instead of relying on its internal `require('puppeteer')` call. On newer Node.js versions (22+, as shipped by Homebrew), a synchronous `require()` of an ESM module triggers `ExperimentalWarning` on every run. Using `addExtra()` bypasses the `require()` entirely and silences the warning. (`all`)
8
+ - `lazyload` (tech detect, react): React framework detection now works for Vite dev-mode servers. Vite's `@vitejs/plugin-react` and `@vitejs/plugin-react-swc` inject an inline `<script>` block containing `import { injectIntoGlobalHook } from "/@react-refresh"` into every dev-served page. Previously, none of the `REACT_MARKERS` matched this content, so dev-mode targets returned `unknown`. Two fixes: (1) `@react-refresh` and `injectIntoGlobalHook` are now included in `REACT_MARKERS` so the inline-script branch of `checkReact` fires immediately; (2) `/@react-refresh` is now a fast-path match in `fetchAndCheck` so any script src pointing at the HMR endpoint is also caught; (3) the intercepted-URL fallback in `frameworkDetect` now recognises `/@react-refresh` as a React signal alongside the existing Next.js / Nuxt / Svelte URL patterns. (`lazyload`, `fingerprint`, `run`)
9
+ - `lazyload` (tech detect, angular): Angular framework detection is now more robust. Three HTML-level signals are checked before fetching `main.js` — `data-beasties-container` (Angular's Beasties SSR/prerendering marker), `ng-version` (set by the Angular runtime on the root component element after bootstrapping, visible in Puppeteer-rendered DOM), and `_nghost-*` view-encapsulation attributes. These checks require no extra HTTP request and work even for zoneless Angular apps (Angular 16+) that omit Zone.js and therefore lack the previous `isAngularZone`/`this.ngZone` patterns. The `main.js` src pattern was also broadened from `main-` (hashed production builds only) to also match `main.js` (unhashed development builds). (`lazyload`, `fingerprint`, `run`)
10
+ - `lazyload`, `map`, `strings`: `.mjs` (ES module) files are now fully supported across all framework pipelines. Previously, extension filters in `downloadQueue.ts`, `downloadFilesUtil.ts`, `downloadLoadedJsUtil.ts`, `react_followImports.ts`, `getReactConnections.ts`, `getViteConnections.ts`, `getAngularConnections.ts`, `vue_resolveHttpClient.ts`, `vue_resolveFetch.ts`, `vue_resolveXhr.ts`, `crossFileResolver.ts`, and `strings/index.ts` only matched `.js` files. Sites that bundle all application code as `.mjs` (e.g. Framer/Rolldown) were silently ignored — all chunks were downloaded but zero files were scanned for connections, strings, or secrets. All affected paths now match both `.js` and `.mjs`. (`run`, `lazyload`, `map`, `strings`)
11
+ - `run` (react): The map step now receives the full `outputDir` (all downloaded hosts) rather than only the target host's subdirectory. Previously, when a React site served all its JS from a CDN host, the map step was pointed at an empty target-host directory and reported zero chunks. Now mirrors the Vue pipeline's directory-passing behaviour so CDN-hosted chunks are analysed correctly. (`run`, `map`)
12
+ - `lazyload` (react): `react_followImports` now discovers dynamic imports written as template literals (``import(`./chunk.mjs`)``) in addition to single/double-quoted strings. Bundlers that use Rolldown (e.g. Framer) emit all dynamic import paths as template literals; the previous regex only matched quoted strings, silently missing all lazily-loaded page and collection chunks. (`lazyload`, `run`)
13
+ - `lazyload`: `.mjs.map` sourcemap files are now handled correctly. The filename extraction regex now prefers the `.mjs.map` suffix over the shorter `.mjs` match, preventing sourcemap JSON content from being written with a `.mjs` extension (which caused Prettier's Babel parser to throw a `SyntaxError` and discard the file). The sourcemap write path now uses the JSON parser for both `.js.map` and `.mjs.map` files. (`lazyload`, `run`)
14
+ - `lazyload` (Puppeteer): `downloadLoadedJsUtil` now uses `waitUntil: "networkidle0"` with a 10 s timeout on `page.goto()`. Previously the call had no timeout and no error handling, so pages whose `load` event never fires (deferred JS, service workers, some SPA patterns) caused the lazyload step to hang indefinitely. Navigation errors and timeouts are now caught and the URLs collected up to that point are returned normally. (`lazyload`, `run`)
15
+ - `lazyload` (Puppeteer): `browser.close()` in `downloadLoadedJsUtil` is now wrapped in a try/catch; if it hangs, the Chrome process is force-killed via `SIGKILL`. Previously a stuck `browser.close()` caused the process to hang after download was otherwise complete. (`lazyload`, `run`)
16
+ - `lazyload` (Puppeteer): All three Puppeteer-using modules (`downloadLoadedJsUtil`, `next_GetLazyResourcesWebpackJs`, `techDetect/index`) now abort non-http/s requests (e.g. `mailto:`, `data:`, `blob:`, `chrome-extension:`, `tel:`) in the request interceptor instead of calling `request.continue()`. Calling `continue()` on these schemes throws an unhandled error and can trigger OS protocol handlers. (`lazyload`, `run`)
17
+ - `lazyload` (Puppeteer): `downloadLoadedJsUtil` and `next_GetLazyResourcesWebpackJs` now pass `--disable-external-protocol-dialog` to Chrome at launch and install an `evaluateOnNewDocument` guard that overrides `window.open` and suppresses clicks on non-http/s anchors. This provides a three-layer defence (Chrome flag + JS intercept + request interceptor abort) ensuring that non-http protocol links on target pages never invoke OS handlers (mail client, phone, etc.). (`lazyload`, `run`)
18
+ - `lazyload` (Puppeteer): All three Puppeteer-using modules now issue a `Page.setDownloadBehavior({ behavior: "deny" })` CDP command immediately after page creation. This prevents accidental file downloads triggered by download-link clicks or JavaScript during crawl, which could block the browser and fill the output directory with unexpected files. (`lazyload`, `run`)
19
+ - `lazyload` (tech detect): After `waitUntil: "load"` resolves, if no framework URL has been captured in `interceptedUrls` yet, a conditional `page.waitForNavigation({ timeout: 5000 })` is now issued to wait for a second navigation event. This catches sites that serve a JS proof-of-work bot challenge (e.g. Vercel's `challenge.v2.min.js`) that fires its own `load` event before calling `window.location` to redirect to the real app. Previously, Puppeteer exited on the challenge page's `load` event and tech detection returned null (exit 10). On sites with no redirect the navigation wait times out after 5 s and detection continues normally. (`lazyload`, `run`)
20
+ - `lazyload`: When `frameworkDetect` returns null and `downloadLoadedJs` is used as a fallback, the downloaded file URL paths are now scanned for `/_next/`, `/_nuxt/`, and `/_app/immutable/` signatures as a second-chance tech detection pass. Previously, sites that serve their framework app at a non-root `basePath` (e.g. Next.js at `/app` instead of `/`) caused the initial Puppeteer-based detection to miss the framework because the intercepted JS paths didn't match any framework pattern, resulting in exit 10. (`lazyload`, `run`)
21
+ - `lazyload` (next_js): `next_parseLayoutJs` now applies a 1.5 MB per-file size guard — checked from both the `Content-Length` response header and the fetched content body — before passing layout files to the AST parser. Files exceeding the limit are skipped with a yellow warning. Previously, Next.js App Router sites with 50+ nested pages each loading a large `layout.js` caused cumulative AST memory of ~2 GB, exhausting the V8 heap and crashing with SIGSEGV (exit 139). (`lazyload`, `run`)
22
+ - `cs-mast`: `--scat`, `--sinc`, `--all-scat-permutations`, `--perm-output`, and `--perm-concurrency` flags were declared in the CLI but not wired to the underlying function, causing a TypeScript compile error (`TS2554: Expected 5 arguments, but got 10`). The function now accepts all five additional parameters: `--scat`/`--sinc` override the active CS-MAST config, and `--all-scat-permutations` runs all 511 non-empty scat subsets in parallel batches and writes one collision file per subset to `--perm-output`. (`cs-mast`)
23
+
24
+ - `refactor -t react-vite`: vendor chunks (`vendor-react-*.js`, `rolldown-runtime-*.js`) that are absent from `mapped.json` are now automatically located in the downloaded assets directory and injected before the refactor pass. Previously, `mapped.json` typically contained only app chunks — vendor chunks are excluded from mapping because they hold only third-party code. This left `vendorExportMaps` empty, so the `rewriteVendorImports` pass had nothing to match against and vendor import statements like `import { d as t } from './vendor-react-CLFLfR9F.js'` survived into the output, causing the Vite build check to fail with "Could not resolve". The fix uses the existing `findAssetsDir`/`findVendorChunkFiles` helpers (already used by the webpack branch) to discover the assets directory from `// File Source:` headers in chunk code.
25
+ - `lazyload` (nuxt): `nuxt_getFromPageSource` now prints the number of new JS files discovered (delta from the global URL set) instead of the running total. Previously the count included every URL already known from earlier in the session.
26
+ - `lazyload` (vue): Each discovery method in `vue_discoverJsFiles` now prints only the count of URL that are genuinely new — URLs already known from earlier methods are excluded. Previously `fromImports.length`, `fromStringRefs.length`, etc. could include URLs already in the accumulator and over-count.
27
+ - Error messages emitted by `makeReq` (fetch failures, cache errors, timeout notices, firewall detection) now use `progressError`/`progressLog` instead of `console.error`/`console.log`, so they are routed through the active progress bar's logger and no longer clutter the bar line when errors occur during a scan.
28
+ - `refactor` (`react-webpack`, `react-vite`): new `--detect-version` flag. When set, the tool fetches per-version `reliable_signatures.json` files from the `shriyanss/cs-mast-s-dataset` HuggingFace bucket (path: `version/react/<bundler>/<version>/<scat>/reliable_signatures.json`) and matches them against CS-MAST signatures generated from the target bundle's chunks. The best-matching React version is used to set `react` and `react-dom` version pins in the refactored output's `package.json` instead of the default `^18.3.1`. Signatures are cached under `~/.js-recon/refactor/version_sigs_cache/` with a 7-day TTL. Supports all webpack versions (react-0.12 through react-19) and all Vite versions (react-16 through react-19) available in the dataset. (`refactor`)
29
+
30
+ ### Added
31
+
32
+ - `fingerprint`: output files are now written incrementally — each result is flushed to disk immediately after detection rather than buffered and written at the end. For text, csv, and jsonl formats the result is appended; for json the full array is rewritten. Output files are also created (and csv headers written) before the worker pool starts, so a partial file always exists if the scan is interrupted. (`fingerprint`)
33
+ - CLI: help output is now colorized. Section titles (`Usage:`, `Options:`, `Commands:`) are bold cyan; flag names are yellow; `<required>` argument placeholders are magenta; `[optional]` argument placeholders are cyan; descriptions are dimmed; and `(default: …)` suffixes are dim italic. Applies to every subcommand.
34
+ - `run`: the pipeline now runs an optional `refactor` pass after the report step for React, Vue, Nuxt, and Next.js targets. The bundler (webpack vs vite) is detected automatically using CS-MAST-S signatures sampled from the `shriyanss/cs-mast-s-dataset` HuggingFace bucket — the same bucket and caching layer used by `refactor --remote-collisions`. If the detected match count meets `--cs-mast-tech-detect-threshold` (default `50`), `refactor` is called with the full tech identifier (e.g. `react-webpack`); otherwise the step is silently skipped. Refactored output is written to `refactored/` (single-URL mode) or `<workingDir>/refactored/` (batch mode). Currently only React bundles have bucket entries; Vue, Nuxt, and Next.js gracefully skip until their signatures are added to the bucket. (`run`, `refactor`)
35
+ - `run`: new `--cs-mast-tech-detect-threshold <n>` flag (default `50`). Sets the minimum number of CS-MAST-S signature matches required to consider a bundler detected for the refactor step. Pass `0` to disable refactor entirely. (`run`)
36
+ - `refactor` (`react-vite`, `react-webpack`): new `--remote-collisions <path>` flag. Accepts a HuggingFace bucket path (e.g. `react/vite/large-0.1.8`) and uses it as the signature source instead of the automatic `TECH_TO_BRANCH` mapping. When the path does not exist in the `shriyanss/cs-mast-s-dataset` bucket the tool exits with code 25. The existing caching layer is fully reused — signatures are cached under `~/.js-recon/refactor/signature_cache/` and the file list is cached in `~/.js-recon/refactor/cs-mast-s-list-cache.json`. Feature directories that contain no collision records in the dataset are skipped during intersection rather than collapsing the result to zero.
37
+ - `refactor` (`react-webpack`, `react-vite`): `--detect-version` now uses multiple scat configurations for improved accuracy. Three new flags control the behaviour: `--detect-version-config` (default `dynamic`), `--detect-version-dynamic-threshold` (default `3`), and `--detect-version-dynamic-conf-purge`. In `dynamic` mode the tool automatically selects up to `--detect-version-dynamic-threshold` scat configs that have non-empty reliable signatures across all known React versions, caches the selection in `~/.js-recon/refactor/config.json`, and aggregates match counts across all configs per version for a stronger detection signal. Alternatively, passing comma-separated scat categories (e.g. `lit,decl,loop,cond`) to `--detect-version-config` fixes the config; the tool validates signatures are non-empty for every version and exits with code 26 if not. Use `--detect-version-dynamic-conf-purge` to force recomputation of the cached config.
38
+
39
+ ### Changed
40
+
41
+ - Set `react/webpack/large-0.1.8` as default cs-mast-s dataset for react + webpack refactoring
42
+
3
43
  ## 1.4.1-alpha.4 - 2026-06-29
4
44
 
5
45
  ### Added
package/CLAUDE.md CHANGED
@@ -77,9 +77,9 @@ npm run start -- <subcommand> [options]
77
77
  1. **Lazyload** — downloads initial JS chunks via Puppeteer; detects framework; sets `globalsUtil.getTech()` to `"next"`
78
78
  2. **Strings** — scans downloaded JS for strings, extracts URL paths → `extracted_urls.json`
79
79
  3. **Lazyload (subsequent requests)** — re-crawls using the extracted paths to fetch dynamically loaded chunks; also fetches `buildId`
80
- 4. **Strings (pass 2)** — re-runs strings on the expanded chunk set; generates `extracted_urls.txt` (permuted) and `extracted_urls-openapi.json`; optionally scans secrets (`--secrets`)
80
+ 4. **Strings (pass 2)** — re-runs strings on the expanded chunk set; generates `extracted_urls.txt` (permuted) and `extracted_urls-openapi.json`; optionally scans secrets (`--secrets`); optionally runs TruffleHog (`--trufflehog`)
81
81
  5. **Lazyload re-pass (step 4.5)** — a second subsequent-requests crawl to pick up chunks for dynamic routes discovered in pass 2
82
- 6. **Strings re-pass (step 4.6)** — strings pass over the re-pass chunks
82
+ 6. **Strings re-pass (step 4.6)** — strings pass over the re-pass chunks; also runs `--secrets` / `--trufflehog` if those flags are set
83
83
  7. **Map** — parses webpack/turbopack bundles; resolves `fetch()` calls and axios usage; generates `mapped.json` and `mapped-openapi.json`; CDN-aware: if JS was served from a different host, `getCdnDir` finds the CDN output dir and passes that to map instead of `outputDir/host`
84
84
  8. **Endpoints** — extracts client-side route paths; uses `___subsequent_requests` directory presence to decide whether to pass a JS directory
85
85
  9. **Analyze** — loads YAML rules (from `-r/--rules` if provided, otherwise default rules cache); runs AST engine and request engine; writes `analyze.json`
@@ -107,7 +107,7 @@ npm run start -- <subcommand> [options]
107
107
 
108
108
  **SvelteKit `adapter-static` (SSG/SPA) boot pattern**: The static builds produce a shell HTML file (`404.html` for SSG, `index.html` for SPA) that contains both `<link rel="modulepreload">` tags for all initial chunks AND the same inline `import()` boot script as adapter-node. `svelte_getFromPageSource` picks up 17+ JS URLs from the modulepreload links plus 2 from the inline script, giving a much larger seed set than the adapter-node case.
109
109
 
110
- **`__vite_mapDeps` path formats**: SvelteKit emits `m.f = ["../nodes/0.js", "../chunks/x.js", ...]` (file-relative paths) inside entry chunks at `_app/immutable/entry/`. Vue and React emit `m.f = ["/assets/chunk.js", ...]` (root-relative paths). `react_followImports` differentiates by checking `p.startsWith("/")` absolute paths resolve against the origin (`baseUrl`), relative paths resolve against the chunk's own URL (`fileUrl`). See `src/lazyload/react/CLAUDE.md` for details.
110
+ **`__vite_mapDeps` path formats**: SvelteKit emits `m.f = ["../nodes/0.js", "../chunks/x.js", ...]` (explicit file-relative paths) inside entry chunks at `_app/immutable/entry/`. Vue and React can emit either `m.f = ["/assets/chunk.js", ...]` (absolute root-relative) or `m.f = ["assets/chunk.js", ...]` (bare root-relative, no leading `/`). `react_followImports` differentiates by checking for a `./` or `../` prefix: only explicitly relative paths resolve against the chunk's own URL (`fileUrl`); all others (absolute `/` or bare names) resolve against the origin (`baseUrl`). Bare names like `assets/x.js` must NOT be resolved against `fileUrl` — when the chunk is inside `assets/`, that would produce a double-directory path. See `src/lazyload/react/CLAUDE.md` for details.
111
111
 
112
112
  ### Batch mode
113
113
 
@@ -200,6 +200,7 @@ Rule categories:
200
200
 
201
201
  - `ast/` — AST-based pattern matching against chunk code (uses `@babel/parser` + `esquery`)
202
202
  - `request/` — OpenAPI/request-level checks against the resolved endpoint list
203
+ - `cs-mast-s/` — CS-MAST-S structural signature matching; each step embeds a PHC string and fires if that node-level hash is found anywhere in the chunk AST. Suitable for regression detection after a vulnerability is confirmed via AST rules. See `src/analyze/engine/csMastSEngine.ts` and `src/analyze/CLAUDE.md` for details.
203
204
 
204
205
  When `-r` points to a single file, only that rule is loaded. When it points to a directory, all `.yml`/`.yaml` files are loaded recursively.
205
206
 
@@ -207,11 +208,137 @@ When `-r` points to a single file, only that rule is loaded. When it points to a
207
208
 
208
209
  **Testing is mandatory for every change.** Before reporting a task complete:
209
210
 
210
- 1. Run `npm run cleanup` to rebuild TypeScript.
211
- 2. Run the `run` subcommand against the target the user provides. Do not use `analyze` or other individual subcommands as a substitute — the `run` subcommand must be used to validate end-to-end behavior.
212
- 3. If the user has not provided a target, ask for one before proceeding.
211
+ 1. Run `npm test` to execute the unit test suite (Vitest).
212
+ 2. Run `npm run cleanup` to rebuild TypeScript.
213
+ 3. Run the `run` subcommand against the target the user provides. Do not use `analyze` or other individual subcommands as a substitute the `run` subcommand must be used to validate end-to-end behavior.
214
+ 4. If the user has not provided a target, ask for one before proceeding.
213
215
 
214
- Typical test invocation:
216
+ ### Rules smoke test (CI)
217
+
218
+ The `rules-smoke-test` GitHub Actions workflow (`.github/workflows/rules-smoke-test.yaml`) runs on every non-main push. It:
219
+
220
+ 1. Checks out `shriyanss/js-recon-labs` and `shriyanss/js-recon-rules` alongside js-recon.
221
+ 2. Builds js-recon and the `next_js/vuln-all-rules` lab app.
222
+ 3. Starts the lab app on port 3001.
223
+ 4. Runs `node build/index.js run -u http://localhost:3001 -r ./js-recon-rules --no-sandbox -y -k`.
224
+ 5. Runs `node scripts/smoke-test.js` which reads `output/localhost:3001/analyze.json` and asserts that all 22 expected rule IDs are present.
225
+
226
+ **`scripts/smoke-test.js`** maintains the `EXPECTED_RULES` list. When a new rule is added to js-recon-rules:
227
+
228
+ - The `next_js/vuln-all-rules` app in js-recon-labs must be updated to seed the new vulnerability.
229
+ - The new rule ID must be appended to `EXPECTED_RULES` in `scripts/smoke-test.js`.
230
+
231
+ The lab app seeds:
232
+
233
+ - 19 AST rules for the `next` tech stack
234
+ - 3 request rules (`api_path`, `admin_api`, `missing_authorization_header`)
235
+ - The Angular-only rule (`detect_angular_bypass_security_trust`) is intentionally excluded.
236
+
237
+ ### Unit tests
238
+
239
+ Unit tests live in `src/__tests__/` and cover pure-logic components. Test framework is **Vitest** (ESM-native, TypeScript-native — no compilation step needed).
240
+
241
+ ```bash
242
+ npm test # run all unit tests once
243
+ npm run test:watch # watch mode
244
+ npm run test:build # legacy build smoke test (node build/index.js -h)
245
+ ```
246
+
247
+ Test files follow the pattern `src/__tests__/<component>/<name>.test.ts`.
248
+
249
+ Covered components:
250
+
251
+ | File | Tests in |
252
+ | ---------------------------------------------------------------------------------------- | ------------------------------------------------------ |
253
+ | `utility/urlUtils.ts` — `getURLDirectory` | `src/__tests__/utility/urlUtils.test.ts` |
254
+ | `utility/replaceUrlPlaceholders.ts` — `replacePlaceholders` | `src/__tests__/utility/replaceUrlPlaceholders.test.ts` |
255
+ | `utility/resolvePath.ts` — `resolvePath` | `src/__tests__/utility/resolvePath.test.ts` |
256
+ | `strings/index.ts` — `extractStrings` | `src/__tests__/strings/extractStrings.test.ts` |
257
+ | `analyze/helpers/validate.ts` — `parseVersion`, `compareVersions`, `isVersionCompatible` | `src/__tests__/analyze/versionCompat.test.ts` |
258
+ | `map/next_js/utils.ts` — `memberChainToString` | `src/__tests__/map/memberChainToString.test.ts` |
259
+ | `fingerprint/index.ts` — `deriveOutputPath` | `src/__tests__/fingerprint/deriveOutputPath.test.ts` |
260
+
261
+ When adding new pure-logic helpers, add a corresponding test file. Components that require Puppeteer, network I/O, or the full pipeline are still validated through the `run` subcommand.
262
+
263
+ ### Writing unit tests
264
+
265
+ **What to test.** Test pure functions: anything that takes plain inputs and returns a value without I/O. The standard pattern for I/O-bound modules is to extract the parse/transform step into an exported pure function and test that. Leave the orchestrator (Puppeteer, `makeRequest`, file writes) untested at the unit level.
266
+
267
+ **Extracting testable functions.** When a function mixes I/O with logic, split it:
268
+
269
+ ```typescript
270
+ // Exported pure function — testable
271
+ export const parseThings = (content: string, baseUrl: string): string[] => { ... };
272
+
273
+ // Orchestrator — not unit-tested
274
+ const myModule = async (url: string): Promise<string[]> => {
275
+ const resp = await makeRequest(url);
276
+ const content = await resp.text();
277
+ return parseThings(content, url);
278
+ };
279
+ ```
280
+
281
+ **Test file structure.** Use `describe` + `it` blocks. Group by function name. Cover: happy path, edge cases (empty input, malformed input), and threshold boundaries (e.g. "fewer than N entries returns []").
282
+
283
+ ```typescript
284
+ import { describe, it, expect } from "vitest";
285
+ import { myPureFunction } from "../../path/to/module.js"; // .js extension required
286
+
287
+ describe("myPureFunction", () => {
288
+ it("extracts X from valid input", () => {
289
+ const result = myPureFunction("...");
290
+ expect(result).toContain("expected");
291
+ });
292
+
293
+ it("returns [] for empty input", () => {
294
+ expect(myPureFunction("")).toEqual([]);
295
+ });
296
+
297
+ it("returns [] for invalid JS", () => {
298
+ expect(myPureFunction("{{{{ not valid")).toEqual([]);
299
+ });
300
+ });
301
+ ```
302
+
303
+ **Imports always use `.js` extension** for local files (ESM project with `"module": "node16"`):
304
+
305
+ ```typescript
306
+ import { fn } from "../../lazyLoad/next_js/myModule.js";
307
+ ```
308
+
309
+ **Constructing Babel AST nodes for tests.** When a function under test requires real Babel AST nodes (with `scope`, `path`, correct `start`/`end` offsets), parse a code snippet in the test and capture the node via a traverse visitor — do not construct AST nodes by hand. Wrap the expression in a `const _x = <expr>;` declaration so `start`/`end` offsets are preserved for any `code.slice()` calls inside the function:
310
+
311
+ ```typescript
312
+ import parser from "@babel/parser";
313
+ import _traverse from "@babel/traverse";
314
+ const traverse = (_traverse.default ?? _traverse) as typeof _traverse.default;
315
+
316
+ function parseExpr(code: string) {
317
+ const src = `const _x = ${code};`;
318
+ const ast = parser.parse(src, { sourceType: "unambiguous", plugins: ["jsx", "typescript"] });
319
+ let node: any;
320
+ traverse(ast, {
321
+ VariableDeclarator(p) {
322
+ node = p.node.init;
323
+ p.stop();
324
+ },
325
+ });
326
+ return { node, src };
327
+ }
328
+ ```
329
+
330
+ **Avoiding GitHub secret scanning.** Strings that look like real secrets (Slack webhook URLs, Stripe keys, etc.) will be blocked by GitHub push protection even in test files. Construct them at runtime from parts arrays:
331
+
332
+ ```typescript
333
+ // BAD — blocked by secret scanner
334
+ const url = "https://hooks.slack.com/services/TABCDEF/BABCDEF/xxxxxxxxxxxx";
335
+
336
+ // GOOD — assembled at runtime
337
+ const parts = ["https://hooks.slack.com/services/T", "ABCDEF/B", "ABCDEF/xxxx"];
338
+ const url = parts.join("");
339
+ ```
340
+
341
+ Typical full test invocation:
215
342
 
216
343
  ```bash
217
344
  npm run cleanup && npm run start -- run -u <target-url> -y -k
@@ -384,6 +511,11 @@ When a user asks to fix or implement a GitHub issue, follow these steps:
384
511
  - `--min-collisions <n>` — minimum occurrences to report (default: 2)
385
512
  - `--co / --collision-output <file>` — write collision data to a file (independent of `--ct`)
386
513
  - `--cf / --collision-format json|csv` — output format (default: csv)
514
+ - `--scat <categories>` — comma-separated scat categories to use (default: `lit,decl,loop,cond`). Overrides the fixed config for this run.
515
+ - `--sinc <nodes>` — comma-separated exact node types to include via sinc (e.g. `IfStatement`).
516
+ - `--all-scat-permutations` — run all 511 non-empty scat subsets and write one collision file per subset to `--perm-output`.
517
+ - `--perm-output <dir>` — output directory for per-permutation files (required with `--all-scat-permutations`).
518
+ - `--perm-concurrency <n>` — parallel permutation workers (default: half of CPU count).
387
519
 
388
520
  **`--co` path resolution:** if the given path is a directory or has no extension, the file is written as `collisions.<fmt>` in the current working directory.
389
521
 
@@ -395,15 +527,28 @@ When a user asks to fix or implement a GitHub issue, follow these steps:
395
527
  npm run build
396
528
  node build/index.js cs-mast -o output --ct --min-collisions 2
397
529
  node build/index.js cs-mast -o output --co output --cf csv # writes ./collisions.csv
530
+ node build/index.js cs-mast -o output --all-scat-permutations --perm-output ./perm-out --cf json
398
531
  ```
399
532
 
400
533
  ## refactor
401
534
 
402
- The `refactor` command supports three techs:
535
+ The `refactor` command supports the following techs:
403
536
 
404
537
  - **`react-webpack`** — webpack 5 React bundles. Splits a numeric module map into per-module ES files, rewrites require→import, recovers JSX. See `src/refactor/react/CLAUDE.md`.
405
538
  - **`react-vite`** — Vite (rolldown) React bundles. Removes CJS interop wrappers, rewrites vendor imports to canonical library imports (`react`, `react/jsx-runtime`, etc.), recovers JSX. Runs a Vite build check after writing output. See `src/refactor/react-vite/CLAUDE.md`.
406
539
  - **`next`** — Next.js bundles (legacy).
540
+ - **`next-turbopack`** — Next.js Turbopack chunks. Handles both turbopack 3-param `func_NNN=(runtime,module,exports)=>{}` and 1-param `func_NNN=(runtime)=>{}` formats plus webpack-style coexisting chunks. See `src/refactor/next/CLAUDE.md`.
541
+ - **`next-webpack`** — Next.js webpack chunks. Input format from `mapped.json`: `NNN:(module,exports,require)=>{}`. Recovers named exports (ODP, require.d), default exports (module.exports=V), re-exports (module.exports=require(N)→export*), and require hoisting. 277/280 modules recovered on a real bundle. Param order: params[0]=module, params[1]=exports, params[2]=require. See `src/refactor/next/CLAUDE.md`.
542
+ - **`vue-webpack`** — Vue.js webpack 4/5 chunks. Container format: `(window.webpackJsonp||[]).push([[chunkIds],{moduleId:function(t,e,r){...}}])`. Each chunk file may contain multiple module functions; each is extracted and transformed. Reuses the Next.js webpack transform (same module param semantics: params[0]=module, params[1]=exports, params[2]=require). See `src/refactor/vue/index.ts`.
543
+ - **`vue-vite`** — Vue 3 + Vite page chunks. The main index chunk (contains `__vccOpts`, all of Vue runtime) is analysed to build an export-alias→canonical-name map. Lazy page chunks then have their index imports rewritten to canonical `import {...} from 'vue'` statements. The `_export_sfc` compiler helper is inlined as a local const. See `src/refactor/vue/vite.ts` and `src/refactor/vue/vendor-analyze-vue.ts`.
544
+
545
+ ### Known react-vite bugs (discovered 2026-07-01, test against js-recon-research/react/20-cve-app)
546
+
547
+ **Multi-chunk file overwrite** — `map` segments each Vite chunk into multiple sub-chunks (one per top-level function). The refactor write path processes sub-chunks sequentially, each overwriting the previous output file. Only the last sub-chunk survives. For chunks with inlined library code followed by the component function (e.g. `ApiProxy`, `Editor`, `Search`), this means the component is the survivor (last in the map order), which is the correct and useful result — but any app-specific helper functions in the same chunk are also lost.
548
+
549
+ **Rename race** — When JSX is detected in a sub-chunk, the write path renames the output file `.js` → `.jsx`. When the same file is written by multiple sub-chunks, the rename is attempted on each JSX-containing chunk; all attempts after the first throw `ENOENT` because the `.js` file was already renamed. The `.jsx` file is correct; the unhandled rejection is noise. Fix: track which output files have already been renamed.
550
+
551
+ **Remote signatures** — The `react/vite/large` HuggingFace bucket is currently empty. The tool falls back gracefully with a warning but remote library stripping is disabled. Symptom: `[!] No remote collisions files found for scat "lit-decl-loop-cond" in branch "react/vite/large"`. Fix: populate the bucket by running CS-MAST-S generation against a corpus of large Vite apps.
407
552
 
408
553
  ### refactor `--collisions <file>` (react-webpack only)
409
554
 
@@ -413,6 +558,37 @@ Plumbing: `src/index.ts` (CLI) → `src/refactor/index.ts` (resolves the path vi
413
558
 
414
559
  The baseline files live in the sibling `js-recon-cs-mast-s/` repo (`baselines/<tech>/<scat>/collisions.json`). See its `README.md` for layout and provenance.
415
560
 
561
+ ### refactor `--detect-version` (react-webpack, react-vite)
562
+
563
+ `refactor -t react-webpack` and `refactor -t react-vite` accept a `--detect-version` flag that uses CS-MAST signatures to detect the React version used in the bundle.
564
+
565
+ **Related flags:**
566
+
567
+ - `--detect-version-config <config>` — `"dynamic"` (default) or comma-separated scat categories (e.g. `lit,decl,loop,cond`).
568
+ - `--detect-version-dynamic-threshold <n>` — number of scat configs to use in dynamic mode (default: 3).
569
+ - `--detect-version-dynamic-conf-purge` — clears the cached dynamic scat config and recomputes.
570
+
571
+ **How it works:**
572
+
573
+ 1. **Scat config resolution** (`resolveVersionDetectionScatDirs()` in `src/refactor/index.ts`):
574
+ - `dynamic` mode: reads cached scat configs from `~/.js-recon/refactor/config.json`. If absent (or purged), calls `selectDynamicScatConfigs()` which lists scat dirs from the HF bucket for a reference version, then validates that each scat dir has non-empty `reliable_signatures.json` for ALL versions. Saves the result to `config.json`.
575
+ - Static mode: parses the user's comma-separated categories, converts to a bucket dir name via `scatToDir()`, validates against all versions, and exits with code 26 if any version's file is empty.
576
+ 2. `generateBundleSignatures()` in `src/refactor/remote/version-detect.ts` runs `cs_mast_init` on every chunk + optional extra code snippets for each selected scat config. This produces a separate signature set per scat.
577
+ 3. For each available React version, `fetchReliableSignatures()` downloads (or loads from cache) the `reliable_signatures.json` per scat config. Match counts are **summed across all scat configs** per version.
578
+ 4. The version with the highest total match count is returned as the detected version.
579
+ 5. The detected version's npm semver is used to pin `react` and `react-dom` in the refactored output's `package.json`.
580
+
581
+ **Caches:**
582
+
583
+ - Signature cache: `~/.js-recon/refactor/version_sigs_cache/<bundler>/<version>/<scatDir>/reliable_signatures.json` + `.cached_at` (7-day TTL).
584
+ - Dynamic config cache: `~/.js-recon/refactor/config.json` (`dynamicVersionDetectionScatConfig` field).
585
+
586
+ **Dataset coverage:** webpack (react-0.12 through react-19), vite (react-16 through react-19).
587
+
588
+ **Important:** the version detection data in the HF bucket was generated with `@shriyanss/cs-mast` v0.1.8. The tool requires cs-mast 0.1.8 or later to produce matching signatures. Using an older cs-mast version will result in zero matches.
589
+
590
+ **Important:** the version detection data in the HF bucket was generated with `@shriyanss/cs-mast` v0.1.8. The tool requires cs-mast 0.1.8 or later to produce matching signatures. Using an older cs-mast version will result in zero matches.
591
+
416
592
  ## Security / confidentiality
417
593
 
418
594
  When a change is informed by behavior observed on a real target (URLs, endpoint names, response shapes, finding details, etc.):