tryassay 0.17.0 → 0.19.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/dist/runtime/__tests__/check-loop.test.js +322 -3
- package/dist/runtime/__tests__/check-loop.test.js.map +1 -1
- package/dist/runtime/check-catalog.d.ts +45 -0
- package/dist/runtime/check-catalog.js +662 -0
- package/dist/runtime/check-catalog.js.map +1 -0
- package/dist/runtime/check-definitions.d.ts +29 -1
- package/dist/runtime/check-definitions.js +335 -3
- package/dist/runtime/check-definitions.js.map +1 -1
- package/dist/runtime/failure-classifier.js +32 -0
- package/dist/runtime/failure-classifier.js.map +1 -1
- package/dist/runtime/integration-verifier.d.ts +9 -1
- package/dist/runtime/integration-verifier.js +118 -1
- package/dist/runtime/integration-verifier.js.map +1 -1
- package/dist/runtime/types.d.ts +33 -3
- package/package.json +1 -1
|
@@ -0,0 +1,662 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CheckCatalog — prebuilt integration check definitions for known failure patterns.
|
|
3
|
+
*
|
|
4
|
+
* Every check here catches a bug that compiles clean but breaks at runtime.
|
|
5
|
+
* Organized by framework/category. Each check specifies a CheckStrategy
|
|
6
|
+
* that the CheckExecutor can run deterministically (no LLM needed).
|
|
7
|
+
*
|
|
8
|
+
* Tier 1: Uses existing executor strategies — ready to execute today.
|
|
9
|
+
* Tier 2: Needs new strategy executor — documented, not yet runnable.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* const checks = getCatalogChecks({ frameworks: ['electron', 'react'] });
|
|
13
|
+
* for (const check of checks) {
|
|
14
|
+
* await checkStore.save(check);
|
|
15
|
+
* }
|
|
16
|
+
*/
|
|
17
|
+
// ── Helpers ──────────────────────────────────────────────────
|
|
18
|
+
function entry(id, name, type, strategy, opts) {
|
|
19
|
+
return {
|
|
20
|
+
id,
|
|
21
|
+
name,
|
|
22
|
+
type,
|
|
23
|
+
strategy,
|
|
24
|
+
severity: opts.severity,
|
|
25
|
+
frameworks: opts.frameworks,
|
|
26
|
+
category: opts.category,
|
|
27
|
+
tier: opts.tier,
|
|
28
|
+
description: opts.description,
|
|
29
|
+
evidenceTemplate: opts.evidenceTemplate,
|
|
30
|
+
status: 'active',
|
|
31
|
+
createdAt: '2026-02-22T00:00:00.000Z',
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
// ════════════════════════════════════════════════════════════════
|
|
35
|
+
// 1. ELECTRON — IPC & Process Boundary
|
|
36
|
+
// ════════════════════════════════════════════════════════════════
|
|
37
|
+
const ELECTRON_IPC = [
|
|
38
|
+
// ── Already implemented (5 checks) ───────────────────────
|
|
39
|
+
entry('cat_electron_001', 'IPC handler coverage', 'ipc_handler_coverage', {
|
|
40
|
+
type: 'cross_reference',
|
|
41
|
+
sourceGlob: '**/preload/**/*.{ts,tsx,js,jsx}',
|
|
42
|
+
sourcePattern: "ipcRenderer\\.invoke\\(\\s*['\"`]([^'\"`]+)['\"`]",
|
|
43
|
+
targetGlob: '**/main/**/*.{ts,tsx,js,jsx}',
|
|
44
|
+
targetPattern: "ipcMain\\.handle\\(\\s*['\"`]{{item}}['\"`]",
|
|
45
|
+
}, {
|
|
46
|
+
severity: 'critical',
|
|
47
|
+
frameworks: ['electron'],
|
|
48
|
+
category: 'Electron IPC',
|
|
49
|
+
tier: 1,
|
|
50
|
+
description: 'Preload invokes an IPC channel that has no handler in the main process. The invoke call will hang or reject at runtime with "No handler registered for channel".',
|
|
51
|
+
evidenceTemplate: "Channel '{match}' invoked in {sourceFile} has no handler in main process.",
|
|
52
|
+
}),
|
|
53
|
+
entry('cat_electron_002', 'Response shape consistency', 'response_shape', {
|
|
54
|
+
type: 'response_shape',
|
|
55
|
+
preloadGlob: '**/preload/**/*.{ts,tsx,js,jsx}',
|
|
56
|
+
handlerGlob: '**/main/**/*.{ts,tsx,js,jsx}',
|
|
57
|
+
invokePattern: "ipcRenderer\\.invoke\\(\\s*['\"`]([^'\"`]+)['\"`]",
|
|
58
|
+
handlePattern: "ipcMain\\.handle\\(\\s*['\"`]([^'\"`]+)['\"`]",
|
|
59
|
+
wrappedReturnPattern: "return\\s*\\{\\s*success:\\s*true,\\s*data:",
|
|
60
|
+
unwrapAccessPattern: "\\.(data)\\b",
|
|
61
|
+
}, {
|
|
62
|
+
severity: 'critical',
|
|
63
|
+
frameworks: ['electron'],
|
|
64
|
+
category: 'Electron IPC',
|
|
65
|
+
tier: 1,
|
|
66
|
+
description: 'Handler wraps response in { success, data } but preload passes it through without unwrapping .data. Renderer accesses result.rows but actual data is at result.data.rows. Compiles clean, breaks at runtime.',
|
|
67
|
+
evidenceTemplate: "Channel '{channel}' in {preloadFile}: handler wraps response but preload does not unwrap .data.",
|
|
68
|
+
}),
|
|
69
|
+
entry('cat_electron_003', 'Stub function in preload', 'stub_function', {
|
|
70
|
+
type: 'pattern_absence',
|
|
71
|
+
fileGlob: '**/preload/**/*.{ts,tsx,js,jsx}',
|
|
72
|
+
pattern: 'async\\s*\\([^)]*\\)\\s*(?::\\s*Promise<[^>]+>)?\\s*=>\\s*\\{\\s*return\\s+\\{[^}]*\\}\\s*;?\\s*\\}',
|
|
73
|
+
}, {
|
|
74
|
+
severity: 'high',
|
|
75
|
+
frameworks: ['electron'],
|
|
76
|
+
category: 'Electron IPC',
|
|
77
|
+
tier: 1,
|
|
78
|
+
description: 'Preload function returns a hardcoded empty object instead of calling ipcRenderer.invoke. The UI renders but all data is empty or undefined.',
|
|
79
|
+
evidenceTemplate: "Stub function found: {match} in {sourceFile}.",
|
|
80
|
+
}),
|
|
81
|
+
// ── New checks ────────────────────────────────────────────
|
|
82
|
+
entry('cat_electron_004', 'contextBridge exposure', 'context_bridge_exposure', {
|
|
83
|
+
type: 'cross_reference',
|
|
84
|
+
sourceGlob: '**/preload/**/*.{ts,tsx,js,jsx}',
|
|
85
|
+
sourcePattern: "ipcRenderer\\.invoke\\(\\s*['\"`]([^'\"`]+)['\"`]",
|
|
86
|
+
targetGlob: '**/preload/**/*.{ts,tsx,js,jsx}',
|
|
87
|
+
targetPattern: "contextBridge\\.exposeInMainWorld",
|
|
88
|
+
}, {
|
|
89
|
+
severity: 'critical',
|
|
90
|
+
frameworks: ['electron'],
|
|
91
|
+
category: 'Electron IPC',
|
|
92
|
+
tier: 1,
|
|
93
|
+
description: 'Preload defines IPC functions but never calls contextBridge.exposeInMainWorld. The renderer has no access to the API object — window.api is undefined. This compiles clean because preload and renderer are separate compilation units.',
|
|
94
|
+
evidenceTemplate: "Preload has {count} IPC invoke call(s) but contextBridge.exposeInMainWorld is {found}.",
|
|
95
|
+
}),
|
|
96
|
+
entry('cat_electron_005', 'IPC channel constant drift', 'ipc_channel_drift', {
|
|
97
|
+
type: 'cross_reference',
|
|
98
|
+
sourceGlob: '**/shared/**/*.{ts,tsx,js,jsx}',
|
|
99
|
+
sourcePattern: "([A-Z_]+)\\s*[:=]\\s*['\"`]([^'\"`]+)['\"`]",
|
|
100
|
+
targetGlob: '**/main/**/*.{ts,tsx,js,jsx}',
|
|
101
|
+
targetPattern: "ipcMain\\.handle\\(\\s*['\"`]{{item}}['\"`]",
|
|
102
|
+
}, {
|
|
103
|
+
severity: 'high',
|
|
104
|
+
frameworks: ['electron'],
|
|
105
|
+
category: 'Electron IPC',
|
|
106
|
+
tier: 1,
|
|
107
|
+
description: 'IPC channel constants defined in shared/types.ts but handler uses a string literal that does not match the constant value. The channel name is subtly different (typo, old name) so the handler never fires.',
|
|
108
|
+
evidenceTemplate: "Constant '{match}' maps to '{value}' but handler string literal does not match.",
|
|
109
|
+
}),
|
|
110
|
+
entry('cat_electron_006', 'IPC send without listener', 'ipc_send_coverage', {
|
|
111
|
+
type: 'cross_reference',
|
|
112
|
+
sourceGlob: '**/main/**/*.{ts,tsx,js,jsx}',
|
|
113
|
+
sourcePattern: "(?:webContents|mainWindow)\\.send\\(\\s*['\"`]([^'\"`]+)['\"`]",
|
|
114
|
+
targetGlob: '**/preload/**/*.{ts,tsx,js,jsx}',
|
|
115
|
+
targetPattern: "ipcRenderer\\.on\\(\\s*['\"`]{{item}}['\"`]",
|
|
116
|
+
}, {
|
|
117
|
+
severity: 'high',
|
|
118
|
+
frameworks: ['electron'],
|
|
119
|
+
category: 'Electron IPC',
|
|
120
|
+
tier: 1,
|
|
121
|
+
description: 'Main process sends a message to the renderer via webContents.send but no ipcRenderer.on listener exists in preload. The message is silently dropped — no error, no data delivered.',
|
|
122
|
+
evidenceTemplate: "Channel '{match}' sent from main but no listener in preload.",
|
|
123
|
+
}),
|
|
124
|
+
];
|
|
125
|
+
// ════════════════════════════════════════════════════════════════
|
|
126
|
+
// 2. ELECTRON — Security & Configuration
|
|
127
|
+
// ════════════════════════════════════════════════════════════════
|
|
128
|
+
const ELECTRON_SECURITY = [
|
|
129
|
+
entry('cat_electron_010', 'nodeIntegration enabled', 'node_integration_security', {
|
|
130
|
+
type: 'pattern_absence',
|
|
131
|
+
fileGlob: '**/main/**/*.{ts,tsx,js,jsx}',
|
|
132
|
+
pattern: 'nodeIntegration\\s*:\\s*true',
|
|
133
|
+
}, {
|
|
134
|
+
severity: 'critical',
|
|
135
|
+
frameworks: ['electron'],
|
|
136
|
+
category: 'Electron Security',
|
|
137
|
+
tier: 1,
|
|
138
|
+
description: 'BrowserWindow created with nodeIntegration: true. This gives the renderer direct access to Node.js APIs, making XSS vulnerabilities equivalent to RCE. Should always be false with contextIsolation: true.',
|
|
139
|
+
evidenceTemplate: "nodeIntegration: true found in {sourceFile}.",
|
|
140
|
+
}),
|
|
141
|
+
entry('cat_electron_011', 'contextIsolation disabled', 'context_isolation_disabled', {
|
|
142
|
+
type: 'pattern_absence',
|
|
143
|
+
fileGlob: '**/main/**/*.{ts,tsx,js,jsx}',
|
|
144
|
+
pattern: 'contextIsolation\\s*:\\s*false',
|
|
145
|
+
}, {
|
|
146
|
+
severity: 'critical',
|
|
147
|
+
frameworks: ['electron'],
|
|
148
|
+
category: 'Electron Security',
|
|
149
|
+
tier: 1,
|
|
150
|
+
description: 'BrowserWindow created with contextIsolation: false. Preload script shares the JavaScript context with the renderer, allowing prototype pollution attacks to escalate to arbitrary code execution.',
|
|
151
|
+
evidenceTemplate: "contextIsolation: false found in {sourceFile}.",
|
|
152
|
+
}),
|
|
153
|
+
entry('cat_electron_012', 'Preload script path validity', 'preload_script_path', {
|
|
154
|
+
type: 'file_reference',
|
|
155
|
+
fileGlob: '**/main/**/*.{ts,tsx,js,jsx}',
|
|
156
|
+
referencePattern: "preload\\s*:\\s*(?:path\\.join\\([^,]+,\\s*)?['\"`]([^'\"`]+)['\"`]",
|
|
157
|
+
}, {
|
|
158
|
+
severity: 'critical',
|
|
159
|
+
frameworks: ['electron'],
|
|
160
|
+
category: 'Electron Security',
|
|
161
|
+
tier: 1,
|
|
162
|
+
description: 'webPreferences.preload points to a path that does not exist on disk. The BrowserWindow loads but window.api is undefined — all IPC calls fail silently. Often caused by build output directory mismatch.',
|
|
163
|
+
evidenceTemplate: "Preload path '{match}' in {sourceFile} does not resolve to an existing file.",
|
|
164
|
+
}),
|
|
165
|
+
entry('cat_electron_013', 'Main/renderer boundary violation', 'main_renderer_boundary', {
|
|
166
|
+
type: 'pattern_absence',
|
|
167
|
+
fileGlob: '**/renderer/**/*.{ts,tsx,js,jsx}',
|
|
168
|
+
pattern: "from\\s+['\"`].*[\\\\/]main[\\\\/]",
|
|
169
|
+
}, {
|
|
170
|
+
severity: 'high',
|
|
171
|
+
frameworks: ['electron'],
|
|
172
|
+
category: 'Electron Security',
|
|
173
|
+
tier: 1,
|
|
174
|
+
description: 'Renderer code directly imports from the main process directory. This breaks Electron process isolation — main-process modules (fs, child_process, etc.) are not available in the renderer with contextIsolation enabled. Compiles because TypeScript resolves the import, but crashes at runtime.',
|
|
175
|
+
evidenceTemplate: "Renderer file {sourceFile} imports from main process: {match}.",
|
|
176
|
+
}),
|
|
177
|
+
entry('cat_electron_014', 'BrowserWindow without loadURL/loadFile', 'browser_window_no_load', {
|
|
178
|
+
type: 'conditional_presence',
|
|
179
|
+
fileGlob: '**/main/**/*.{ts,tsx,js,jsx}',
|
|
180
|
+
conditionPattern: 'new\\s+BrowserWindow\\s*\\(',
|
|
181
|
+
requiredPattern: '\\.(loadURL|loadFile)\\s*\\(',
|
|
182
|
+
}, {
|
|
183
|
+
severity: 'critical',
|
|
184
|
+
frameworks: ['electron'],
|
|
185
|
+
category: 'Electron Security',
|
|
186
|
+
tier: 1,
|
|
187
|
+
description: 'BrowserWindow is created but never loads a URL or file. The window appears as a blank white screen. Usually caused by the loadURL call being conditional or in a callback that never fires.',
|
|
188
|
+
evidenceTemplate: "BrowserWindow created in {sourceFile} but no .loadURL or .loadFile found.",
|
|
189
|
+
}),
|
|
190
|
+
];
|
|
191
|
+
// ════════════════════════════════════════════════════════════════
|
|
192
|
+
// 3. REACT — Routing & Navigation
|
|
193
|
+
// ════════════════════════════════════════════════════════════════
|
|
194
|
+
const REACT_ROUTING = [
|
|
195
|
+
entry('cat_react_001', 'Router provider missing', 'routing_provider', {
|
|
196
|
+
type: 'pattern_presence',
|
|
197
|
+
fileGlob: '**/*.{tsx,jsx}',
|
|
198
|
+
pattern: '\\b(BrowserRouter|HashRouter|MemoryRouter|RouterProvider|createBrowserRouter)\\b',
|
|
199
|
+
requireIn: 'any',
|
|
200
|
+
contextGlob: '**/*.{tsx,jsx}',
|
|
201
|
+
contextPattern: '\\b(useNavigate|useParams|useLocation|useSearchParams)\\b',
|
|
202
|
+
}, {
|
|
203
|
+
severity: 'critical',
|
|
204
|
+
frameworks: ['react'],
|
|
205
|
+
category: 'React Routing',
|
|
206
|
+
tier: 1,
|
|
207
|
+
description: 'Components call useNavigate/useParams/useLocation but no Router provider wraps the component tree. Crashes at runtime: "useNavigate() may be used only in the context of a <Router> component."',
|
|
208
|
+
evidenceTemplate: "Router hook usage found but no Router provider in any file.",
|
|
209
|
+
}),
|
|
210
|
+
entry('cat_react_002', 'Unreachable page component', 'unreachable_page', {
|
|
211
|
+
type: 'import_reachability',
|
|
212
|
+
pageGlob: '**/pages/**/*.{tsx,jsx}',
|
|
213
|
+
entryPattern: 'createRoot|ReactDOM\\.render',
|
|
214
|
+
}, {
|
|
215
|
+
severity: 'high',
|
|
216
|
+
frameworks: ['react'],
|
|
217
|
+
category: 'React Routing',
|
|
218
|
+
tier: 1,
|
|
219
|
+
description: 'A page component exists in the pages directory but is never imported into the router or any component reachable from the entry point. The page is dead code — no URL navigates to it.',
|
|
220
|
+
evidenceTemplate: "Page '{match}' is not imported from any file reachable from the entry point.",
|
|
221
|
+
}),
|
|
222
|
+
entry('cat_react_003', 'Link to undefined route', 'link_route_mismatch', {
|
|
223
|
+
type: 'cross_reference',
|
|
224
|
+
sourceGlob: '**/*.{tsx,jsx}',
|
|
225
|
+
sourcePattern: '<(?:Link|NavLink)\\s+[^>]*to=["\']([^"\']+)["\']',
|
|
226
|
+
targetGlob: '**/*.{tsx,jsx}',
|
|
227
|
+
targetPattern: '<Route\\s+[^>]*path=["\']{{item}}["\']',
|
|
228
|
+
}, {
|
|
229
|
+
severity: 'high',
|
|
230
|
+
frameworks: ['react'],
|
|
231
|
+
category: 'React Routing',
|
|
232
|
+
tier: 1,
|
|
233
|
+
description: 'A Link or NavLink points to a path that has no matching Route definition. Clicking the link navigates but renders nothing (blank area) or the catch-all 404 route.',
|
|
234
|
+
evidenceTemplate: "Link to='{match}' in {sourceFile} has no matching Route path.",
|
|
235
|
+
}),
|
|
236
|
+
];
|
|
237
|
+
// ════════════════════════════════════════════════════════════════
|
|
238
|
+
// 4. REACT — Context & State
|
|
239
|
+
// ════════════════════════════════════════════════════════════════
|
|
240
|
+
const REACT_CONTEXT = [
|
|
241
|
+
entry('cat_react_010', 'Missing error boundary', 'missing_error_boundary', {
|
|
242
|
+
type: 'conditional_presence',
|
|
243
|
+
fileGlob: '**/*.{tsx,jsx}',
|
|
244
|
+
conditionPattern: '<Route\\s',
|
|
245
|
+
requiredPattern: '<ErrorBoundary|errorElement',
|
|
246
|
+
}, {
|
|
247
|
+
severity: 'medium',
|
|
248
|
+
frameworks: ['react'],
|
|
249
|
+
category: 'React Context',
|
|
250
|
+
tier: 1,
|
|
251
|
+
description: 'Route definitions exist but no ErrorBoundary or errorElement wraps them. Any unhandled error in a route component crashes the entire app with a white screen instead of showing a fallback UI.',
|
|
252
|
+
evidenceTemplate: "Routes found in {sourceFile} but no ErrorBoundary or errorElement.",
|
|
253
|
+
}),
|
|
254
|
+
entry('cat_react_011', 'Missing Suspense for lazy import', 'missing_suspense_boundary', {
|
|
255
|
+
type: 'conditional_presence',
|
|
256
|
+
fileGlob: '**/*.{tsx,jsx}',
|
|
257
|
+
conditionPattern: '\\blazy\\s*\\(',
|
|
258
|
+
requiredPattern: '<Suspense',
|
|
259
|
+
}, {
|
|
260
|
+
severity: 'critical',
|
|
261
|
+
frameworks: ['react'],
|
|
262
|
+
category: 'React Context',
|
|
263
|
+
tier: 1,
|
|
264
|
+
description: 'React.lazy() is used to code-split a component but no <Suspense> boundary wraps it. React throws: "A component suspended while responding to synchronous input." The lazy component never renders.',
|
|
265
|
+
evidenceTemplate: "lazy() import found in {sourceFile} but no <Suspense> boundary.",
|
|
266
|
+
}),
|
|
267
|
+
entry('cat_react_012', 'Context used without Provider', 'context_without_provider', {
|
|
268
|
+
type: 'cross_reference',
|
|
269
|
+
sourceGlob: '**/*.{tsx,jsx}',
|
|
270
|
+
sourcePattern: 'useContext\\(\\s*(\\w+)\\s*\\)',
|
|
271
|
+
targetGlob: '**/*.{tsx,jsx}',
|
|
272
|
+
targetPattern: '<{{item}}\\.Provider',
|
|
273
|
+
}, {
|
|
274
|
+
severity: 'critical',
|
|
275
|
+
frameworks: ['react'],
|
|
276
|
+
category: 'React Context',
|
|
277
|
+
tier: 1,
|
|
278
|
+
description: 'A component calls useContext(SomeContext) but no <SomeContext.Provider> exists anywhere in the component tree. The hook returns the default value (often undefined), causing silent data loss or null reference errors.',
|
|
279
|
+
evidenceTemplate: "useContext({match}) in {sourceFile} but no <{match}.Provider> found.",
|
|
280
|
+
}),
|
|
281
|
+
];
|
|
282
|
+
// ════════════════════════════════════════════════════════════════
|
|
283
|
+
// 5. NEXT.JS — Server/Client Boundary
|
|
284
|
+
// ════════════════════════════════════════════════════════════════
|
|
285
|
+
const NEXTJS = [
|
|
286
|
+
entry('cat_next_001', "'use client' missing for hooks", 'use_client_missing', {
|
|
287
|
+
type: 'conditional_presence',
|
|
288
|
+
fileGlob: '**/app/**/*.{tsx,jsx}',
|
|
289
|
+
conditionPattern: '\\b(useState|useEffect|useRef|useCallback|useMemo|useReducer|useContext)\\b',
|
|
290
|
+
requiredPattern: "['\"]use client['\"]",
|
|
291
|
+
}, {
|
|
292
|
+
severity: 'critical',
|
|
293
|
+
frameworks: ['nextjs'],
|
|
294
|
+
category: 'Next.js Boundary',
|
|
295
|
+
tier: 1,
|
|
296
|
+
description: "File in the app directory uses React hooks (useState, useEffect, etc.) but is missing the 'use client' directive. Next.js treats it as a server component, which cannot use hooks. Build may pass but page crashes at runtime.",
|
|
297
|
+
evidenceTemplate: "File {sourceFile} uses {match} but is missing 'use client' directive.",
|
|
298
|
+
}),
|
|
299
|
+
entry('cat_next_002', 'Server component imports client module', 'server_imports_client', {
|
|
300
|
+
type: 'cross_reference',
|
|
301
|
+
sourceGlob: '**/app/**/*.{tsx,jsx}',
|
|
302
|
+
sourcePattern: "import\\s+.*from\\s+['\"]([^'\"]+)['\"]",
|
|
303
|
+
targetGlob: '**/*.{tsx,jsx}',
|
|
304
|
+
targetPattern: "['\"]use client['\"]",
|
|
305
|
+
}, {
|
|
306
|
+
severity: 'high',
|
|
307
|
+
frameworks: ['nextjs'],
|
|
308
|
+
category: 'Next.js Boundary',
|
|
309
|
+
tier: 1,
|
|
310
|
+
description: 'A server component imports a module that contains "use client". This is allowed for component imports (renders as client boundary) but breaks for importing hooks or client-only utilities directly.',
|
|
311
|
+
evidenceTemplate: "Server component {sourceFile} imports client module '{match}'.",
|
|
312
|
+
}),
|
|
313
|
+
entry('cat_next_003', 'API route missing HTTP method export', 'api_route_export', {
|
|
314
|
+
type: 'conditional_presence',
|
|
315
|
+
fileGlob: '**/app/api/**/route.{ts,tsx,js,jsx}',
|
|
316
|
+
conditionPattern: '.', // any content (file exists = condition met)
|
|
317
|
+
requiredPattern: 'export\\s+(?:async\\s+)?function\\s+(GET|POST|PUT|PATCH|DELETE|HEAD|OPTIONS)',
|
|
318
|
+
}, {
|
|
319
|
+
severity: 'critical',
|
|
320
|
+
frameworks: ['nextjs'],
|
|
321
|
+
category: 'Next.js Boundary',
|
|
322
|
+
tier: 1,
|
|
323
|
+
description: 'An API route file exists at app/api/.../route.ts but does not export any HTTP method handler (GET, POST, etc.). Requests to this endpoint return 405 Method Not Allowed.',
|
|
324
|
+
evidenceTemplate: "API route {sourceFile} has no exported HTTP method handler.",
|
|
325
|
+
}),
|
|
326
|
+
entry('cat_next_004', 'generateMetadata in client component', 'metadata_in_client', {
|
|
327
|
+
type: 'conditional_presence',
|
|
328
|
+
fileGlob: '**/app/**/*.{tsx,jsx}',
|
|
329
|
+
conditionPattern: 'generateMetadata|export\\s+const\\s+metadata',
|
|
330
|
+
requiredPattern: "(?!['\"]use client['\"])", // must NOT have 'use client'
|
|
331
|
+
}, {
|
|
332
|
+
severity: 'high',
|
|
333
|
+
frameworks: ['nextjs'],
|
|
334
|
+
category: 'Next.js Boundary',
|
|
335
|
+
tier: 1,
|
|
336
|
+
description: "File exports generateMetadata or metadata constant but also has 'use client'. Metadata APIs only work in server components. The metadata is silently ignored — page renders but has no title/description.",
|
|
337
|
+
evidenceTemplate: "Metadata export in {sourceFile} will be ignored because file is a client component.",
|
|
338
|
+
}),
|
|
339
|
+
];
|
|
340
|
+
// ════════════════════════════════════════════════════════════════
|
|
341
|
+
// 6. API INTEGRATION
|
|
342
|
+
// ════════════════════════════════════════════════════════════════
|
|
343
|
+
const API_INTEGRATION = [
|
|
344
|
+
entry('cat_api_001', 'Fetch URL matches no API route', 'fetch_route_mismatch', {
|
|
345
|
+
type: 'cross_reference',
|
|
346
|
+
sourceGlob: '**/*.{ts,tsx,js,jsx}',
|
|
347
|
+
sourcePattern: "fetch\\(\\s*['\"`]/api/([^'\"`\\s]+)['\"`]",
|
|
348
|
+
targetGlob: '**/api/**/*.{ts,tsx,js,jsx}',
|
|
349
|
+
targetPattern: '{{item}}',
|
|
350
|
+
}, {
|
|
351
|
+
severity: 'critical',
|
|
352
|
+
frameworks: ['nextjs', 'express', 'general'],
|
|
353
|
+
category: 'API Integration',
|
|
354
|
+
tier: 1,
|
|
355
|
+
description: 'Frontend calls fetch("/api/some-path") but no API route handler exists for that path. The request returns 404 at runtime. Common after renaming or reorganizing API routes.',
|
|
356
|
+
evidenceTemplate: "fetch('/api/{match}') in {sourceFile} has no matching API route handler.",
|
|
357
|
+
}),
|
|
358
|
+
entry('cat_api_002', 'Supabase function call without edge function', 'supabase_function_missing', {
|
|
359
|
+
type: 'cross_reference',
|
|
360
|
+
sourceGlob: '**/*.{ts,tsx,js,jsx}',
|
|
361
|
+
sourcePattern: "supabase\\.functions\\.invoke\\(\\s*['\"`]([^'\"`]+)['\"`]",
|
|
362
|
+
targetGlob: '**/supabase/functions/*/index.ts',
|
|
363
|
+
targetPattern: '{{item}}',
|
|
364
|
+
}, {
|
|
365
|
+
severity: 'critical',
|
|
366
|
+
frameworks: ['supabase'],
|
|
367
|
+
category: 'API Integration',
|
|
368
|
+
tier: 1,
|
|
369
|
+
description: 'Frontend calls supabase.functions.invoke("function-name") but no edge function directory with that name exists. The call returns a FunctionNotFound error at runtime.',
|
|
370
|
+
evidenceTemplate: "supabase.functions.invoke('{match}') in {sourceFile} has no matching edge function.",
|
|
371
|
+
}),
|
|
372
|
+
entry('cat_api_003', 'Missing error handling on fetch', 'fetch_no_error_handling', {
|
|
373
|
+
type: 'conditional_presence',
|
|
374
|
+
fileGlob: '**/*.{ts,tsx,js,jsx}',
|
|
375
|
+
conditionPattern: '\\bfetch\\(',
|
|
376
|
+
requiredPattern: '(?:\\.catch|try\\s*\\{|response\\.ok|response\\.status)',
|
|
377
|
+
}, {
|
|
378
|
+
severity: 'medium',
|
|
379
|
+
frameworks: ['general'],
|
|
380
|
+
category: 'API Integration',
|
|
381
|
+
tier: 1,
|
|
382
|
+
description: 'fetch() call without any error handling — no .catch(), no try/catch, no response.ok check. Network failures or server errors crash the app with an unhandled promise rejection instead of showing an error state.',
|
|
383
|
+
evidenceTemplate: "fetch() in {sourceFile} has no error handling.",
|
|
384
|
+
}),
|
|
385
|
+
];
|
|
386
|
+
// ════════════════════════════════════════════════════════════════
|
|
387
|
+
// 7. DATABASE & SCHEMA
|
|
388
|
+
// ════════════════════════════════════════════════════════════════
|
|
389
|
+
const DATABASE = [
|
|
390
|
+
entry('cat_db_001', 'Supabase table without RLS', 'missing_rls_policy', {
|
|
391
|
+
type: 'pattern_absence',
|
|
392
|
+
fileGlob: '**/migrations/**/*.sql',
|
|
393
|
+
pattern: 'CREATE\\s+TABLE\\s+(?!.*ALTER\\s+TABLE.*ENABLE\\s+ROW\\s+LEVEL\\s+SECURITY)',
|
|
394
|
+
}, {
|
|
395
|
+
severity: 'critical',
|
|
396
|
+
frameworks: ['supabase'],
|
|
397
|
+
category: 'Database',
|
|
398
|
+
tier: 1,
|
|
399
|
+
description: 'A migration creates a table but never enables RLS (Row Level Security). With Supabase, this means the anon key has full read/write access to the table from the client — a data exposure vulnerability.',
|
|
400
|
+
evidenceTemplate: "Table created in {sourceFile} but RLS not enabled.",
|
|
401
|
+
}),
|
|
402
|
+
entry('cat_db_002', 'Code references non-existent column', 'schema_column_mismatch', {
|
|
403
|
+
type: 'cross_reference',
|
|
404
|
+
sourceGlob: '**/*.{ts,tsx,js,jsx}',
|
|
405
|
+
sourcePattern: "\\.select\\(['\"`]([^'\"`]+)['\"`]\\)",
|
|
406
|
+
targetGlob: '**/migrations/**/*.sql',
|
|
407
|
+
targetPattern: '{{item}}',
|
|
408
|
+
}, {
|
|
409
|
+
severity: 'high',
|
|
410
|
+
frameworks: ['supabase', 'general'],
|
|
411
|
+
category: 'Database',
|
|
412
|
+
tier: 1,
|
|
413
|
+
description: 'Application code references a column name in a select/insert/update that does not appear in any migration file. The query fails at runtime with "column does not exist". Often caused by renaming columns in code but not in migrations.',
|
|
414
|
+
evidenceTemplate: "Column '{match}' referenced in {sourceFile} not found in any migration.",
|
|
415
|
+
}),
|
|
416
|
+
entry('cat_db_003', 'Query return type mismatch', 'query_type_mismatch', {
|
|
417
|
+
type: 'response_shape',
|
|
418
|
+
preloadGlob: '**/*.{ts,tsx,js,jsx}',
|
|
419
|
+
handlerGlob: '**/migrations/**/*.sql',
|
|
420
|
+
invokePattern: "\\.from\\(['\"`]([^'\"`]+)['\"`]\\)",
|
|
421
|
+
handlePattern: "CREATE\\s+TABLE\\s+(?:public\\.)?([\\w]+)",
|
|
422
|
+
wrappedReturnPattern: 'data:\\s*\\{',
|
|
423
|
+
unwrapAccessPattern: '\\.data\\b',
|
|
424
|
+
}, {
|
|
425
|
+
severity: 'medium',
|
|
426
|
+
frameworks: ['supabase'],
|
|
427
|
+
category: 'Database',
|
|
428
|
+
tier: 1,
|
|
429
|
+
description: 'Supabase query returns { data, error } but code accesses the result directly without destructuring. The component renders undefined instead of the queried data.',
|
|
430
|
+
evidenceTemplate: "Query on table '{match}' in {sourceFile}: result not properly destructured.",
|
|
431
|
+
}),
|
|
432
|
+
];
|
|
433
|
+
// ════════════════════════════════════════════════════════════════
|
|
434
|
+
// 8. AUTH & SECURITY
|
|
435
|
+
// ════════════════════════════════════════════════════════════════
|
|
436
|
+
const AUTH_SECURITY = [
|
|
437
|
+
entry('cat_auth_001', 'Unprotected route with sensitive data', 'unprotected_route', {
|
|
438
|
+
type: 'cross_reference',
|
|
439
|
+
sourceGlob: '**/*.{tsx,jsx}',
|
|
440
|
+
sourcePattern: '<Route\\s+[^>]*path=["\']/(admin|dashboard|settings|profile|billing)["\']',
|
|
441
|
+
targetGlob: '**/*.{tsx,jsx}',
|
|
442
|
+
targetPattern: '(ProtectedRoute|RequireAuth|AuthGuard|withAuth)',
|
|
443
|
+
}, {
|
|
444
|
+
severity: 'critical',
|
|
445
|
+
frameworks: ['react', 'nextjs'],
|
|
446
|
+
category: 'Auth & Security',
|
|
447
|
+
tier: 1,
|
|
448
|
+
description: 'Routes for sensitive areas (admin, dashboard, settings, billing) exist but no auth guard component (ProtectedRoute, RequireAuth, etc.) is found. Unauthenticated users can navigate directly to these routes.',
|
|
449
|
+
evidenceTemplate: "Route '{match}' has no auth guard. Found auth guard components: {found}.",
|
|
450
|
+
}),
|
|
451
|
+
entry('cat_auth_002', 'Environment variable missing from .env', 'env_var_missing', {
|
|
452
|
+
type: 'cross_reference',
|
|
453
|
+
sourceGlob: '**/*.{ts,tsx,js,jsx}',
|
|
454
|
+
sourcePattern: 'process\\.env\\.([A-Z_][A-Z0-9_]+)',
|
|
455
|
+
targetGlob: '**/.env*',
|
|
456
|
+
targetPattern: '{{item}}\\s*=',
|
|
457
|
+
}, {
|
|
458
|
+
severity: 'high',
|
|
459
|
+
frameworks: ['general'],
|
|
460
|
+
category: 'Auth & Security',
|
|
461
|
+
tier: 1,
|
|
462
|
+
description: 'Code references process.env.SOME_VAR but the variable is not defined in any .env file. The value is undefined at runtime, causing silent failures in API calls, auth flows, or configuration.',
|
|
463
|
+
evidenceTemplate: "process.env.{match} in {sourceFile} not defined in any .env file.",
|
|
464
|
+
}),
|
|
465
|
+
entry('cat_auth_003', 'Hardcoded secret in source', 'hardcoded_secret', {
|
|
466
|
+
type: 'pattern_absence',
|
|
467
|
+
fileGlob: '**/*.{ts,tsx,js,jsx}',
|
|
468
|
+
pattern: "(?:api[_-]?key|secret|password|token|auth)\\s*[:=]\\s*['\"`][A-Za-z0-9+/=_-]{20,}['\"`]",
|
|
469
|
+
allowIn: ['**/*.test.*', '**/*.spec.*', '**/__tests__/**'],
|
|
470
|
+
}, {
|
|
471
|
+
severity: 'critical',
|
|
472
|
+
frameworks: ['general'],
|
|
473
|
+
category: 'Auth & Security',
|
|
474
|
+
tier: 1,
|
|
475
|
+
description: 'An API key, secret, password, or token is hardcoded as a string literal in source code. This is a security vulnerability — the secret is exposed in version control and built artifacts.',
|
|
476
|
+
evidenceTemplate: "Hardcoded secret found in {sourceFile}: {match}.",
|
|
477
|
+
}),
|
|
478
|
+
entry('cat_auth_004', 'Missing CORS configuration', 'cors_missing', {
|
|
479
|
+
type: 'conditional_presence',
|
|
480
|
+
fileGlob: '**/*.{ts,js}',
|
|
481
|
+
conditionPattern: "(?:express\\(\\)|createServer|Hono|Fastify)",
|
|
482
|
+
requiredPattern: '(?:cors\\(|Access-Control-Allow-Origin|allowedOrigins)',
|
|
483
|
+
}, {
|
|
484
|
+
severity: 'high',
|
|
485
|
+
frameworks: ['express', 'general'],
|
|
486
|
+
category: 'Auth & Security',
|
|
487
|
+
tier: 1,
|
|
488
|
+
description: 'An HTTP server is created but no CORS configuration is applied. Cross-origin requests from the frontend fail with "Access-Control-Allow-Origin" errors. The API works from same-origin (dev) but breaks in production.',
|
|
489
|
+
evidenceTemplate: "Server created in {sourceFile} but no CORS configuration found.",
|
|
490
|
+
}),
|
|
491
|
+
];
|
|
492
|
+
// ════════════════════════════════════════════════════════════════
|
|
493
|
+
// 9. EXPRESS / NODE API
|
|
494
|
+
// ════════════════════════════════════════════════════════════════
|
|
495
|
+
const EXPRESS_NODE = [
|
|
496
|
+
entry('cat_express_001', 'Auth middleware after route handlers', 'middleware_ordering', {
|
|
497
|
+
type: 'ordering',
|
|
498
|
+
fileGlob: '**/*.{ts,js}',
|
|
499
|
+
beforePattern: '(?:app|router)\\.use\\(\\s*(?:auth|requireAuth|verifyToken|passport\\.authenticate)',
|
|
500
|
+
afterPattern: '(?:app|router)\\.(get|post|put|patch|delete)\\(',
|
|
501
|
+
}, {
|
|
502
|
+
severity: 'critical',
|
|
503
|
+
frameworks: ['express'],
|
|
504
|
+
category: 'Express/Node',
|
|
505
|
+
tier: 1,
|
|
506
|
+
description: 'Auth middleware is registered after route handlers. Express middleware runs in registration order, so routes defined before the auth middleware are unprotected. All requests to those routes bypass authentication.',
|
|
507
|
+
evidenceTemplate: "Auth middleware in {sourceFile} registered at line {line} but route handlers start at line {beforeLine}.",
|
|
508
|
+
}),
|
|
509
|
+
entry('cat_express_002', 'Missing error middleware', 'missing_error_middleware', {
|
|
510
|
+
type: 'conditional_presence',
|
|
511
|
+
fileGlob: '**/*.{ts,js}',
|
|
512
|
+
conditionPattern: "(?:express\\(\\)|new\\s+Hono)",
|
|
513
|
+
requiredPattern: "app\\.use\\(\\s*\\(\\s*(?:err|error)\\s*,\\s*req\\s*,\\s*res\\s*,\\s*next",
|
|
514
|
+
}, {
|
|
515
|
+
severity: 'high',
|
|
516
|
+
frameworks: ['express'],
|
|
517
|
+
category: 'Express/Node',
|
|
518
|
+
tier: 1,
|
|
519
|
+
description: 'Express app has no error handling middleware (4-parameter function). Unhandled errors in route handlers crash the process or leak stack traces to the client.',
|
|
520
|
+
evidenceTemplate: "Express app in {sourceFile} has no error handling middleware.",
|
|
521
|
+
}),
|
|
522
|
+
entry('cat_express_003', 'Async route without error handling', 'async_route_unhandled', {
|
|
523
|
+
type: 'conditional_presence',
|
|
524
|
+
fileGlob: '**/*.{ts,js}',
|
|
525
|
+
conditionPattern: '\\.(get|post|put|delete)\\(.*async\\s*\\(',
|
|
526
|
+
requiredPattern: '(?:try\\s*\\{|asyncHandler|express-async-errors)',
|
|
527
|
+
}, {
|
|
528
|
+
severity: 'high',
|
|
529
|
+
frameworks: ['express'],
|
|
530
|
+
category: 'Express/Node',
|
|
531
|
+
tier: 1,
|
|
532
|
+
description: 'An async route handler has no try/catch and no express-async-errors or asyncHandler wrapper. If the async function throws, Express does not catch it — the request hangs until timeout, and the error is an unhandled rejection.',
|
|
533
|
+
evidenceTemplate: "Async route handler in {sourceFile} has no error wrapping.",
|
|
534
|
+
}),
|
|
535
|
+
entry('cat_express_004', 'Missing body parser for POST', 'missing_body_parser', {
|
|
536
|
+
type: 'conditional_presence',
|
|
537
|
+
fileGlob: '**/*.{ts,js}',
|
|
538
|
+
conditionPattern: '\\.(post|put|patch)\\(.*req\\.body',
|
|
539
|
+
requiredPattern: '(?:express\\.json\\(|bodyParser\\.json|express\\.urlencoded)',
|
|
540
|
+
}, {
|
|
541
|
+
severity: 'high',
|
|
542
|
+
frameworks: ['express'],
|
|
543
|
+
category: 'Express/Node',
|
|
544
|
+
tier: 1,
|
|
545
|
+
description: 'Route handler accesses req.body but no JSON body parser middleware is registered. req.body is undefined — the handler receives no data from POST/PUT requests.',
|
|
546
|
+
evidenceTemplate: "Route in {sourceFile} uses req.body but no body parser middleware found.",
|
|
547
|
+
}),
|
|
548
|
+
];
|
|
549
|
+
// ════════════════════════════════════════════════════════════════
|
|
550
|
+
// 10. GENERAL WEB
|
|
551
|
+
// ════════════════════════════════════════════════════════════════
|
|
552
|
+
const GENERAL_WEB = [
|
|
553
|
+
entry('cat_web_001', 'Form action without handler', 'form_action_missing', {
|
|
554
|
+
type: 'cross_reference',
|
|
555
|
+
sourceGlob: '**/*.{tsx,jsx}',
|
|
556
|
+
sourcePattern: "<form\\s+[^>]*action=['\"]([^'\"]+)['\"]",
|
|
557
|
+
targetGlob: '**/api/**/*.{ts,tsx,js,jsx}',
|
|
558
|
+
targetPattern: '{{item}}',
|
|
559
|
+
}, {
|
|
560
|
+
severity: 'high',
|
|
561
|
+
frameworks: ['general'],
|
|
562
|
+
category: 'General Web',
|
|
563
|
+
tier: 1,
|
|
564
|
+
description: 'A form has an action attribute pointing to a URL that has no server-side handler. Form submission navigates to a 404 or returns an error.',
|
|
565
|
+
evidenceTemplate: "Form action='{match}' in {sourceFile} has no matching handler.",
|
|
566
|
+
}),
|
|
567
|
+
entry('cat_web_002', 'Event listener for never-dispatched event', 'dead_event_listener', {
|
|
568
|
+
type: 'cross_reference',
|
|
569
|
+
sourceGlob: '**/*.{ts,tsx,js,jsx}',
|
|
570
|
+
sourcePattern: "addEventListener\\(\\s*['\"`]([^'\"`]+)['\"`]",
|
|
571
|
+
targetGlob: '**/*.{ts,tsx,js,jsx}',
|
|
572
|
+
targetPattern: "dispatchEvent.*['\"`]{{item}}['\"`]|new\\s+CustomEvent\\(['\"`]{{item}}['\"`]",
|
|
573
|
+
}, {
|
|
574
|
+
severity: 'medium',
|
|
575
|
+
frameworks: ['general'],
|
|
576
|
+
category: 'General Web',
|
|
577
|
+
tier: 1,
|
|
578
|
+
description: 'Code registers an event listener for a custom event that is never dispatched anywhere in the codebase. The listener is dead code — the callback never fires.',
|
|
579
|
+
evidenceTemplate: "Listener for '{match}' in {sourceFile} but event never dispatched.",
|
|
580
|
+
}),
|
|
581
|
+
entry('cat_web_003', 'CSS class used but not defined', 'css_class_missing', {
|
|
582
|
+
type: 'cross_reference',
|
|
583
|
+
sourceGlob: '**/*.{tsx,jsx}',
|
|
584
|
+
sourcePattern: "className=['\"`]([\\w-]+)['\"`]",
|
|
585
|
+
targetGlob: '**/*.{css,scss,less}',
|
|
586
|
+
targetPattern: '\\.{{item}}\\b',
|
|
587
|
+
}, {
|
|
588
|
+
severity: 'medium',
|
|
589
|
+
frameworks: ['general'],
|
|
590
|
+
category: 'General Web',
|
|
591
|
+
tier: 1,
|
|
592
|
+
description: 'A component uses className="some-class" but no CSS file defines .some-class. The styling is silently missing. Note: does not apply to Tailwind or CSS-in-JS which generate classes dynamically.',
|
|
593
|
+
evidenceTemplate: "className='{match}' in {sourceFile} has no CSS definition.",
|
|
594
|
+
}),
|
|
595
|
+
entry('cat_web_004', 'Image src points to missing asset', 'asset_reference_broken', {
|
|
596
|
+
type: 'file_reference',
|
|
597
|
+
fileGlob: '**/*.{tsx,jsx,html}',
|
|
598
|
+
referencePattern: "(?:src|href)=['\"`](/[^'\"`]+\\.(?:png|jpg|jpeg|gif|svg|webp|ico))['\"`]",
|
|
599
|
+
baseDir: 'public',
|
|
600
|
+
}, {
|
|
601
|
+
severity: 'medium',
|
|
602
|
+
frameworks: ['general'],
|
|
603
|
+
category: 'General Web',
|
|
604
|
+
tier: 1,
|
|
605
|
+
description: 'An img src or link href points to a static asset path that does not exist in the public directory. The image renders as a broken icon. Often caused by moving or renaming assets without updating references.',
|
|
606
|
+
evidenceTemplate: "Asset '{match}' referenced in {sourceFile} not found in public/.",
|
|
607
|
+
}),
|
|
608
|
+
];
|
|
609
|
+
// ════════════════════════════════════════════════════════════════
|
|
610
|
+
// FULL CATALOG
|
|
611
|
+
// ════════════════════════════════════════════════════════════════
|
|
612
|
+
const ALL_CHECKS = [
|
|
613
|
+
...ELECTRON_IPC,
|
|
614
|
+
...ELECTRON_SECURITY,
|
|
615
|
+
...REACT_ROUTING,
|
|
616
|
+
...REACT_CONTEXT,
|
|
617
|
+
...NEXTJS,
|
|
618
|
+
...API_INTEGRATION,
|
|
619
|
+
...DATABASE,
|
|
620
|
+
...AUTH_SECURITY,
|
|
621
|
+
...EXPRESS_NODE,
|
|
622
|
+
...GENERAL_WEB,
|
|
623
|
+
];
|
|
624
|
+
/**
|
|
625
|
+
* Get prebuilt checks from the catalog, optionally filtered by framework.
|
|
626
|
+
*
|
|
627
|
+
* @param opts.frameworks Only return checks relevant to these frameworks.
|
|
628
|
+
* If omitted, returns all checks.
|
|
629
|
+
* @param opts.tier Only return checks of this tier (1 = executable, 2 = needs strategy).
|
|
630
|
+
* If omitted, returns both tiers.
|
|
631
|
+
*/
|
|
632
|
+
export function getCatalogChecks(opts) {
|
|
633
|
+
let checks = [...ALL_CHECKS];
|
|
634
|
+
if (opts?.frameworks?.length) {
|
|
635
|
+
const fw = new Set(opts.frameworks.map((f) => f.toLowerCase()));
|
|
636
|
+
checks = checks.filter((c) => c.frameworks.some((f) => fw.has(f)) ||
|
|
637
|
+
c.frameworks.includes('general'));
|
|
638
|
+
}
|
|
639
|
+
if (opts?.tier) {
|
|
640
|
+
checks = checks.filter((c) => c.tier === opts.tier);
|
|
641
|
+
}
|
|
642
|
+
return checks;
|
|
643
|
+
}
|
|
644
|
+
/** Summary statistics for the catalog. */
|
|
645
|
+
export function getCatalogStats() {
|
|
646
|
+
const byCategory = {};
|
|
647
|
+
const byFramework = {};
|
|
648
|
+
for (const check of ALL_CHECKS) {
|
|
649
|
+
byCategory[check.category] = (byCategory[check.category] ?? 0) + 1;
|
|
650
|
+
for (const fw of check.frameworks) {
|
|
651
|
+
byFramework[fw] = (byFramework[fw] ?? 0) + 1;
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
return {
|
|
655
|
+
total: ALL_CHECKS.length,
|
|
656
|
+
tier1: ALL_CHECKS.filter((c) => c.tier === 1).length,
|
|
657
|
+
tier2: ALL_CHECKS.filter((c) => c.tier === 2).length,
|
|
658
|
+
byCategory,
|
|
659
|
+
byFramework,
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
//# sourceMappingURL=check-catalog.js.map
|