@apiposture/cli 1.0.1
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/.apiposture.json.example +56 -0
- package/.github/workflows/publish.yml +38 -0
- package/.github/workflows/test.yml +42 -0
- package/LICENSE +21 -0
- package/README.md +156 -0
- package/dist/cli/commands/license/activate.d.ts +3 -0
- package/dist/cli/commands/license/activate.js +35 -0
- package/dist/cli/commands/license/deactivate.d.ts +3 -0
- package/dist/cli/commands/license/deactivate.js +28 -0
- package/dist/cli/commands/license/status.d.ts +3 -0
- package/dist/cli/commands/license/status.js +36 -0
- package/dist/cli/commands/scan.d.ts +3 -0
- package/dist/cli/commands/scan.js +211 -0
- package/dist/cli/options.d.ts +27 -0
- package/dist/cli/options.js +30 -0
- package/dist/core/analysis/project-analyzer.d.ts +16 -0
- package/dist/core/analysis/project-analyzer.js +54 -0
- package/dist/core/analysis/source-file-loader.d.ts +32 -0
- package/dist/core/analysis/source-file-loader.js +155 -0
- package/dist/core/authorization/authorization-extractor.d.ts +11 -0
- package/dist/core/authorization/authorization-extractor.js +2 -0
- package/dist/core/authorization/express-auth-extractor.d.ts +10 -0
- package/dist/core/authorization/express-auth-extractor.js +106 -0
- package/dist/core/authorization/global-auth-analyzer.d.ts +12 -0
- package/dist/core/authorization/global-auth-analyzer.js +74 -0
- package/dist/core/authorization/nestjs-auth-extractor.d.ts +13 -0
- package/dist/core/authorization/nestjs-auth-extractor.js +142 -0
- package/dist/core/configuration/config-loader.d.ts +27 -0
- package/dist/core/configuration/config-loader.js +72 -0
- package/dist/core/configuration/suppression-matcher.d.ts +14 -0
- package/dist/core/configuration/suppression-matcher.js +79 -0
- package/dist/core/discovery/discoverer-interface.d.ts +7 -0
- package/dist/core/discovery/discoverer-interface.js +2 -0
- package/dist/core/discovery/express-discoverer.d.ts +20 -0
- package/dist/core/discovery/express-discoverer.js +223 -0
- package/dist/core/discovery/fastify-discoverer.d.ts +19 -0
- package/dist/core/discovery/fastify-discoverer.js +249 -0
- package/dist/core/discovery/framework-detector.d.ts +9 -0
- package/dist/core/discovery/framework-detector.js +61 -0
- package/dist/core/discovery/index.d.ts +8 -0
- package/dist/core/discovery/index.js +8 -0
- package/dist/core/discovery/koa-discoverer.d.ts +16 -0
- package/dist/core/discovery/koa-discoverer.js +151 -0
- package/dist/core/discovery/nestjs-discoverer.d.ts +16 -0
- package/dist/core/discovery/nestjs-discoverer.js +180 -0
- package/dist/core/discovery/route-group-registry.d.ts +18 -0
- package/dist/core/discovery/route-group-registry.js +50 -0
- package/dist/core/licensing/license-context.d.ts +17 -0
- package/dist/core/licensing/license-context.js +15 -0
- package/dist/core/licensing/license-features.d.ts +14 -0
- package/dist/core/licensing/license-features.js +47 -0
- package/dist/core/models/authorization-info.d.ts +13 -0
- package/dist/core/models/authorization-info.js +25 -0
- package/dist/core/models/endpoint-type.d.ts +8 -0
- package/dist/core/models/endpoint-type.js +12 -0
- package/dist/core/models/endpoint.d.ts +16 -0
- package/dist/core/models/endpoint.js +16 -0
- package/dist/core/models/finding.d.ts +19 -0
- package/dist/core/models/finding.js +8 -0
- package/dist/core/models/http-method.d.ts +14 -0
- package/dist/core/models/http-method.js +25 -0
- package/dist/core/models/index.d.ts +10 -0
- package/dist/core/models/index.js +10 -0
- package/dist/core/models/scan-result.d.ts +21 -0
- package/dist/core/models/scan-result.js +35 -0
- package/dist/core/models/security-classification.d.ts +8 -0
- package/dist/core/models/security-classification.js +12 -0
- package/dist/core/models/severity.d.ts +11 -0
- package/dist/core/models/severity.js +23 -0
- package/dist/core/models/source-location.d.ts +7 -0
- package/dist/core/models/source-location.js +4 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +23 -0
- package/dist/licensing/license-manager.d.ts +38 -0
- package/dist/licensing/license-manager.js +184 -0
- package/dist/output/accessibility-helper.d.ts +22 -0
- package/dist/output/accessibility-helper.js +98 -0
- package/dist/output/formatter-interface.d.ts +11 -0
- package/dist/output/formatter-interface.js +2 -0
- package/dist/output/index.d.ts +6 -0
- package/dist/output/index.js +6 -0
- package/dist/output/json-formatter.d.ts +7 -0
- package/dist/output/json-formatter.js +72 -0
- package/dist/output/markdown-formatter.d.ts +10 -0
- package/dist/output/markdown-formatter.js +114 -0
- package/dist/output/terminal-formatter.d.ts +12 -0
- package/dist/output/terminal-formatter.js +82 -0
- package/dist/rules/consistency/controller-action-conflict.d.ts +19 -0
- package/dist/rules/consistency/controller-action-conflict.js +40 -0
- package/dist/rules/consistency/missing-auth-on-writes.d.ts +21 -0
- package/dist/rules/consistency/missing-auth-on-writes.js +59 -0
- package/dist/rules/exposure/allow-anonymous-on-write.d.ts +20 -0
- package/dist/rules/exposure/allow-anonymous-on-write.js +42 -0
- package/dist/rules/exposure/public-without-explicit-intent.d.ts +20 -0
- package/dist/rules/exposure/public-without-explicit-intent.js +58 -0
- package/dist/rules/index.d.ts +11 -0
- package/dist/rules/index.js +11 -0
- package/dist/rules/privilege/excessive-role-access.d.ts +20 -0
- package/dist/rules/privilege/excessive-role-access.js +36 -0
- package/dist/rules/privilege/weak-role-naming.d.ts +20 -0
- package/dist/rules/privilege/weak-role-naming.js +50 -0
- package/dist/rules/rule-engine.d.ts +15 -0
- package/dist/rules/rule-engine.js +52 -0
- package/dist/rules/rule-interface.d.ts +16 -0
- package/dist/rules/rule-interface.js +2 -0
- package/dist/rules/surface/sensitive-route-keywords.d.ts +20 -0
- package/dist/rules/surface/sensitive-route-keywords.js +63 -0
- package/dist/rules/surface/unprotected-endpoint.d.ts +20 -0
- package/dist/rules/surface/unprotected-endpoint.js +61 -0
- package/package.json +60 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
2
|
+
import { getLineAndColumn, findNodes } from '../analysis/source-file-loader.js';
|
|
3
|
+
import { createEndpoint } from '../models/endpoint.js';
|
|
4
|
+
import { EndpointType } from '../models/endpoint-type.js';
|
|
5
|
+
import { HttpMethod, parseHttpMethod } from '../models/http-method.js';
|
|
6
|
+
import { createDefaultAuthorizationInfo, determineClassification, } from '../models/authorization-info.js';
|
|
7
|
+
const FASTIFY_HTTP_METHODS = new Set([
|
|
8
|
+
'get', 'post', 'put', 'delete', 'patch', 'options', 'head', 'all'
|
|
9
|
+
]);
|
|
10
|
+
const FASTIFY_IDENTIFIERS = new Set(['fastify', 'server', 'app', 'instance']);
|
|
11
|
+
const AUTH_HOOK_PATTERNS = [
|
|
12
|
+
/authenticate/i,
|
|
13
|
+
/verify/i,
|
|
14
|
+
/jwt/i,
|
|
15
|
+
/auth/i,
|
|
16
|
+
/guard/i,
|
|
17
|
+
/protected/i,
|
|
18
|
+
];
|
|
19
|
+
export class FastifyDiscoverer {
|
|
20
|
+
name = 'Fastify';
|
|
21
|
+
async discover(file) {
|
|
22
|
+
const endpoints = [];
|
|
23
|
+
const { sourceFile, filePath } = file;
|
|
24
|
+
const callExpressions = findNodes(sourceFile, ts.isCallExpression);
|
|
25
|
+
for (const callExpr of callExpressions) {
|
|
26
|
+
// Check for fastify.get(), fastify.post(), etc.
|
|
27
|
+
const shorthandEndpoint = this.processShorthandRoute(callExpr, sourceFile, filePath);
|
|
28
|
+
if (shorthandEndpoint) {
|
|
29
|
+
endpoints.push(shorthandEndpoint);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
// Check for fastify.route({ method, url, ... })
|
|
33
|
+
const routeEndpoint = this.processRouteMethod(callExpr, sourceFile, filePath);
|
|
34
|
+
if (routeEndpoint) {
|
|
35
|
+
endpoints.push(routeEndpoint);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return endpoints;
|
|
39
|
+
}
|
|
40
|
+
processShorthandRoute(callExpr, sourceFile, filePath) {
|
|
41
|
+
if (!ts.isPropertyAccessExpression(callExpr.expression)) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
const propAccess = callExpr.expression;
|
|
45
|
+
const methodName = propAccess.name.text.toLowerCase();
|
|
46
|
+
if (!FASTIFY_HTTP_METHODS.has(methodName)) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
const callerName = this.getCallerName(propAccess.expression);
|
|
50
|
+
if (!callerName || !this.isFastifyIdentifier(callerName)) {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
const args = callExpr.arguments;
|
|
54
|
+
if (args.length === 0) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
// First arg is route path
|
|
58
|
+
const routePath = this.extractStringValue(args[0]);
|
|
59
|
+
if (!routePath) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
// Extract options/handler
|
|
63
|
+
let handlerName = 'anonymous';
|
|
64
|
+
let authorization = createDefaultAuthorizationInfo();
|
|
65
|
+
// Check for options object: fastify.get('/path', { preHandler: [] }, handler)
|
|
66
|
+
if (args.length >= 2 && ts.isObjectLiteralExpression(args[1])) {
|
|
67
|
+
const options = args[1];
|
|
68
|
+
authorization = this.extractAuthFromOptions(options, sourceFile);
|
|
69
|
+
if (args.length >= 3) {
|
|
70
|
+
handlerName = this.extractHandlerName(args[2], sourceFile);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
else if (args.length >= 2) {
|
|
74
|
+
handlerName = this.extractHandlerName(args[1], sourceFile);
|
|
75
|
+
}
|
|
76
|
+
authorization.classification = determineClassification(authorization);
|
|
77
|
+
const location = getLineAndColumn(sourceFile, callExpr.getStart(sourceFile));
|
|
78
|
+
return createEndpoint({
|
|
79
|
+
route: this.normalizePath(routePath),
|
|
80
|
+
method: parseHttpMethod(methodName.toUpperCase()) ?? HttpMethod.GET,
|
|
81
|
+
handlerName,
|
|
82
|
+
type: EndpointType.Fastify,
|
|
83
|
+
location: {
|
|
84
|
+
filePath,
|
|
85
|
+
line: location.line,
|
|
86
|
+
column: location.column,
|
|
87
|
+
},
|
|
88
|
+
authorization,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
processRouteMethod(callExpr, sourceFile, filePath) {
|
|
92
|
+
if (!ts.isPropertyAccessExpression(callExpr.expression)) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
const propAccess = callExpr.expression;
|
|
96
|
+
if (propAccess.name.text !== 'route') {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
const callerName = this.getCallerName(propAccess.expression);
|
|
100
|
+
if (!callerName || !this.isFastifyIdentifier(callerName)) {
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
const args = callExpr.arguments;
|
|
104
|
+
if (args.length === 0 || !ts.isObjectLiteralExpression(args[0])) {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
const options = args[0];
|
|
108
|
+
let method = null;
|
|
109
|
+
let url = null;
|
|
110
|
+
let handlerName = 'handler';
|
|
111
|
+
const authorization = createDefaultAuthorizationInfo();
|
|
112
|
+
for (const prop of options.properties) {
|
|
113
|
+
if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
const propName = prop.name.text;
|
|
117
|
+
if (propName === 'method') {
|
|
118
|
+
const methodStr = this.extractStringValue(prop.initializer);
|
|
119
|
+
if (methodStr) {
|
|
120
|
+
method = parseHttpMethod(methodStr.toUpperCase()) ?? null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (propName === 'url') {
|
|
124
|
+
url = this.extractStringValue(prop.initializer);
|
|
125
|
+
}
|
|
126
|
+
if (propName === 'handler') {
|
|
127
|
+
handlerName = this.extractHandlerName(prop.initializer, sourceFile);
|
|
128
|
+
}
|
|
129
|
+
if (propName === 'preHandler' || propName === 'onRequest') {
|
|
130
|
+
this.extractHooksAuth(prop.initializer, sourceFile, authorization);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (!method || !url) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
authorization.classification = determineClassification(authorization);
|
|
137
|
+
const location = getLineAndColumn(sourceFile, callExpr.getStart(sourceFile));
|
|
138
|
+
return createEndpoint({
|
|
139
|
+
route: this.normalizePath(url),
|
|
140
|
+
method,
|
|
141
|
+
handlerName,
|
|
142
|
+
type: EndpointType.Fastify,
|
|
143
|
+
location: {
|
|
144
|
+
filePath,
|
|
145
|
+
line: location.line,
|
|
146
|
+
column: location.column,
|
|
147
|
+
},
|
|
148
|
+
authorization,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
extractAuthFromOptions(options, sourceFile) {
|
|
152
|
+
const auth = createDefaultAuthorizationInfo();
|
|
153
|
+
for (const prop of options.properties) {
|
|
154
|
+
if (!ts.isPropertyAssignment(prop) || !ts.isIdentifier(prop.name)) {
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const propName = prop.name.text;
|
|
158
|
+
if (propName === 'preHandler' || propName === 'onRequest') {
|
|
159
|
+
this.extractHooksAuth(prop.initializer, sourceFile, auth);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return auth;
|
|
163
|
+
}
|
|
164
|
+
extractHooksAuth(node, sourceFile, auth) {
|
|
165
|
+
const hooks = [];
|
|
166
|
+
if (ts.isArrayLiteralExpression(node)) {
|
|
167
|
+
for (const elem of node.elements) {
|
|
168
|
+
const hookName = this.extractHookName(elem, sourceFile);
|
|
169
|
+
if (hookName) {
|
|
170
|
+
hooks.push(hookName);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
else {
|
|
175
|
+
const hookName = this.extractHookName(node, sourceFile);
|
|
176
|
+
if (hookName) {
|
|
177
|
+
hooks.push(hookName);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
auth.middlewareChain.push(...hooks);
|
|
181
|
+
for (const hook of hooks) {
|
|
182
|
+
if (this.isAuthHook(hook)) {
|
|
183
|
+
auth.isAuthenticated = true;
|
|
184
|
+
auth.guardNames.push(hook);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
extractHookName(node, sourceFile) {
|
|
189
|
+
if (ts.isIdentifier(node)) {
|
|
190
|
+
return node.text;
|
|
191
|
+
}
|
|
192
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
193
|
+
return node.getText(sourceFile);
|
|
194
|
+
}
|
|
195
|
+
if (ts.isCallExpression(node)) {
|
|
196
|
+
if (ts.isIdentifier(node.expression)) {
|
|
197
|
+
return node.expression.text;
|
|
198
|
+
}
|
|
199
|
+
if (ts.isPropertyAccessExpression(node.expression)) {
|
|
200
|
+
return node.expression.getText(sourceFile);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
isAuthHook(hookName) {
|
|
206
|
+
return AUTH_HOOK_PATTERNS.some((pattern) => pattern.test(hookName));
|
|
207
|
+
}
|
|
208
|
+
getCallerName(expression) {
|
|
209
|
+
if (ts.isIdentifier(expression)) {
|
|
210
|
+
return expression.text;
|
|
211
|
+
}
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
isFastifyIdentifier(name) {
|
|
215
|
+
if (FASTIFY_IDENTIFIERS.has(name.toLowerCase())) {
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
return name.toLowerCase().includes('fastify');
|
|
219
|
+
}
|
|
220
|
+
extractStringValue(node) {
|
|
221
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
|
|
222
|
+
return node.text;
|
|
223
|
+
}
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
extractHandlerName(node, sourceFile) {
|
|
227
|
+
if (ts.isIdentifier(node)) {
|
|
228
|
+
return node.text;
|
|
229
|
+
}
|
|
230
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
231
|
+
return node.getText(sourceFile);
|
|
232
|
+
}
|
|
233
|
+
if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
|
|
234
|
+
return 'anonymous';
|
|
235
|
+
}
|
|
236
|
+
return 'unknown';
|
|
237
|
+
}
|
|
238
|
+
normalizePath(path) {
|
|
239
|
+
if (!path.startsWith('/')) {
|
|
240
|
+
path = '/' + path;
|
|
241
|
+
}
|
|
242
|
+
path = path.replace(/\/+/g, '/');
|
|
243
|
+
if (path.length > 1 && path.endsWith('/')) {
|
|
244
|
+
path = path.slice(0, -1);
|
|
245
|
+
}
|
|
246
|
+
return path;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
//# sourceMappingURL=fastify-discoverer.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { EndpointType } from '../models/endpoint-type.js';
|
|
2
|
+
export interface DetectedFrameworks {
|
|
3
|
+
frameworks: EndpointType[];
|
|
4
|
+
confidence: Record<EndpointType, number>;
|
|
5
|
+
}
|
|
6
|
+
export declare class FrameworkDetector {
|
|
7
|
+
detect(projectPath: string): DetectedFrameworks;
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=framework-detector.d.ts.map
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { EndpointType } from '../models/endpoint-type.js';
|
|
4
|
+
export class FrameworkDetector {
|
|
5
|
+
detect(projectPath) {
|
|
6
|
+
const result = {
|
|
7
|
+
frameworks: [],
|
|
8
|
+
confidence: {
|
|
9
|
+
[EndpointType.Express]: 0,
|
|
10
|
+
[EndpointType.NestJS]: 0,
|
|
11
|
+
[EndpointType.Fastify]: 0,
|
|
12
|
+
[EndpointType.Koa]: 0,
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
// Read package.json
|
|
16
|
+
const packageJsonPath = path.join(projectPath, 'package.json');
|
|
17
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
let packageJson;
|
|
21
|
+
try {
|
|
22
|
+
const content = fs.readFileSync(packageJsonPath, 'utf-8');
|
|
23
|
+
packageJson = JSON.parse(content);
|
|
24
|
+
}
|
|
25
|
+
catch {
|
|
26
|
+
return result;
|
|
27
|
+
}
|
|
28
|
+
const deps = {
|
|
29
|
+
...packageJson.dependencies,
|
|
30
|
+
...packageJson.devDependencies,
|
|
31
|
+
};
|
|
32
|
+
// Check for Express
|
|
33
|
+
if (deps.express) {
|
|
34
|
+
result.confidence[EndpointType.Express] = 100;
|
|
35
|
+
}
|
|
36
|
+
// Check for NestJS
|
|
37
|
+
if (deps['@nestjs/core'] || deps['@nestjs/common']) {
|
|
38
|
+
result.confidence[EndpointType.NestJS] = 100;
|
|
39
|
+
}
|
|
40
|
+
// Check for Fastify
|
|
41
|
+
if (deps.fastify) {
|
|
42
|
+
result.confidence[EndpointType.Fastify] = 100;
|
|
43
|
+
}
|
|
44
|
+
// Check for Koa
|
|
45
|
+
if (deps.koa) {
|
|
46
|
+
result.confidence[EndpointType.Koa] = 100;
|
|
47
|
+
// If koa-router is present, increase confidence
|
|
48
|
+
if (deps['koa-router'] || deps['@koa/router']) {
|
|
49
|
+
result.confidence[EndpointType.Koa] = 100;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
// Build frameworks list (those with confidence > 0)
|
|
53
|
+
for (const [framework, confidence] of Object.entries(result.confidence)) {
|
|
54
|
+
if (confidence > 0) {
|
|
55
|
+
result.frameworks.push(framework);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
//# sourceMappingURL=framework-detector.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from './discoverer-interface.js';
|
|
2
|
+
export * from './express-discoverer.js';
|
|
3
|
+
export * from './nestjs-discoverer.js';
|
|
4
|
+
export * from './fastify-discoverer.js';
|
|
5
|
+
export * from './koa-discoverer.js';
|
|
6
|
+
export * from './route-group-registry.js';
|
|
7
|
+
export * from './framework-detector.js';
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from './discoverer-interface.js';
|
|
2
|
+
export * from './express-discoverer.js';
|
|
3
|
+
export * from './nestjs-discoverer.js';
|
|
4
|
+
export * from './fastify-discoverer.js';
|
|
5
|
+
export * from './koa-discoverer.js';
|
|
6
|
+
export * from './route-group-registry.js';
|
|
7
|
+
export * from './framework-detector.js';
|
|
8
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { EndpointDiscoverer } from './discoverer-interface.js';
|
|
2
|
+
import { LoadedSourceFile } from '../analysis/source-file-loader.js';
|
|
3
|
+
import { Endpoint } from '../models/endpoint.js';
|
|
4
|
+
export declare class KoaDiscoverer implements EndpointDiscoverer {
|
|
5
|
+
readonly name = "Koa";
|
|
6
|
+
discover(file: LoadedSourceFile): Promise<Endpoint[]>;
|
|
7
|
+
private processRouteCall;
|
|
8
|
+
private buildAuthorizationInfo;
|
|
9
|
+
private isAuthMiddleware;
|
|
10
|
+
private getCallerName;
|
|
11
|
+
private isKoaRouterIdentifier;
|
|
12
|
+
private extractStringValue;
|
|
13
|
+
private extractMiddlewareName;
|
|
14
|
+
private normalizePath;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=koa-discoverer.d.ts.map
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
2
|
+
import { getLineAndColumn, findNodes } from '../analysis/source-file-loader.js';
|
|
3
|
+
import { createEndpoint } from '../models/endpoint.js';
|
|
4
|
+
import { EndpointType } from '../models/endpoint-type.js';
|
|
5
|
+
import { HttpMethod, parseHttpMethod } from '../models/http-method.js';
|
|
6
|
+
import { createDefaultAuthorizationInfo, determineClassification, } from '../models/authorization-info.js';
|
|
7
|
+
const KOA_HTTP_METHODS = new Set([
|
|
8
|
+
'get', 'post', 'put', 'delete', 'patch', 'options', 'head', 'all'
|
|
9
|
+
]);
|
|
10
|
+
const KOA_ROUTER_IDENTIFIERS = new Set(['router', 'koaRouter', 'apiRouter']);
|
|
11
|
+
const AUTH_MIDDLEWARE_PATTERNS = [
|
|
12
|
+
/authenticate/i,
|
|
13
|
+
/auth/i,
|
|
14
|
+
/jwt/i,
|
|
15
|
+
/passport/i,
|
|
16
|
+
/protect/i,
|
|
17
|
+
/guard/i,
|
|
18
|
+
/verify/i,
|
|
19
|
+
];
|
|
20
|
+
export class KoaDiscoverer {
|
|
21
|
+
name = 'Koa';
|
|
22
|
+
async discover(file) {
|
|
23
|
+
const endpoints = [];
|
|
24
|
+
const { sourceFile, filePath } = file;
|
|
25
|
+
const callExpressions = findNodes(sourceFile, ts.isCallExpression);
|
|
26
|
+
for (const callExpr of callExpressions) {
|
|
27
|
+
const endpoint = this.processRouteCall(callExpr, sourceFile, filePath);
|
|
28
|
+
if (endpoint) {
|
|
29
|
+
endpoints.push(endpoint);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return endpoints;
|
|
33
|
+
}
|
|
34
|
+
processRouteCall(callExpr, sourceFile, filePath) {
|
|
35
|
+
if (!ts.isPropertyAccessExpression(callExpr.expression)) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const propAccess = callExpr.expression;
|
|
39
|
+
const methodName = propAccess.name.text.toLowerCase();
|
|
40
|
+
if (!KOA_HTTP_METHODS.has(methodName)) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const callerName = this.getCallerName(propAccess.expression);
|
|
44
|
+
if (!callerName || !this.isKoaRouterIdentifier(callerName)) {
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
const args = callExpr.arguments;
|
|
48
|
+
if (args.length === 0) {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
// First arg is route path
|
|
52
|
+
const routePath = this.extractStringValue(args[0]);
|
|
53
|
+
if (!routePath) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
// Extract middleware chain and handler
|
|
57
|
+
const middlewares = [];
|
|
58
|
+
let handlerName = 'anonymous';
|
|
59
|
+
for (let i = 1; i < args.length; i++) {
|
|
60
|
+
const arg = args[i];
|
|
61
|
+
const name = this.extractMiddlewareName(arg, sourceFile);
|
|
62
|
+
if (i === args.length - 1) {
|
|
63
|
+
// Last argument is the handler
|
|
64
|
+
handlerName = name ?? 'anonymous';
|
|
65
|
+
}
|
|
66
|
+
else if (name) {
|
|
67
|
+
middlewares.push(name);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
// Build authorization info
|
|
71
|
+
const authorization = this.buildAuthorizationInfo(middlewares);
|
|
72
|
+
const location = getLineAndColumn(sourceFile, callExpr.getStart(sourceFile));
|
|
73
|
+
return createEndpoint({
|
|
74
|
+
route: this.normalizePath(routePath),
|
|
75
|
+
method: parseHttpMethod(methodName.toUpperCase()) ?? HttpMethod.GET,
|
|
76
|
+
handlerName,
|
|
77
|
+
type: EndpointType.Koa,
|
|
78
|
+
location: {
|
|
79
|
+
filePath,
|
|
80
|
+
line: location.line,
|
|
81
|
+
column: location.column,
|
|
82
|
+
},
|
|
83
|
+
authorization,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
buildAuthorizationInfo(middlewares) {
|
|
87
|
+
const auth = createDefaultAuthorizationInfo();
|
|
88
|
+
auth.middlewareChain = middlewares;
|
|
89
|
+
for (const middleware of middlewares) {
|
|
90
|
+
if (this.isAuthMiddleware(middleware)) {
|
|
91
|
+
auth.isAuthenticated = true;
|
|
92
|
+
auth.guardNames.push(middleware);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
auth.classification = determineClassification(auth);
|
|
96
|
+
return auth;
|
|
97
|
+
}
|
|
98
|
+
isAuthMiddleware(name) {
|
|
99
|
+
return AUTH_MIDDLEWARE_PATTERNS.some((pattern) => pattern.test(name));
|
|
100
|
+
}
|
|
101
|
+
getCallerName(expression) {
|
|
102
|
+
if (ts.isIdentifier(expression)) {
|
|
103
|
+
return expression.text;
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
isKoaRouterIdentifier(name) {
|
|
108
|
+
if (KOA_ROUTER_IDENTIFIERS.has(name)) {
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
const lower = name.toLowerCase();
|
|
112
|
+
return lower.includes('router') || lower.includes('koa');
|
|
113
|
+
}
|
|
114
|
+
extractStringValue(node) {
|
|
115
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
|
|
116
|
+
return node.text;
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
extractMiddlewareName(node, sourceFile) {
|
|
121
|
+
if (ts.isIdentifier(node)) {
|
|
122
|
+
return node.text;
|
|
123
|
+
}
|
|
124
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
125
|
+
return node.getText(sourceFile);
|
|
126
|
+
}
|
|
127
|
+
if (ts.isCallExpression(node)) {
|
|
128
|
+
if (ts.isIdentifier(node.expression)) {
|
|
129
|
+
return node.expression.text;
|
|
130
|
+
}
|
|
131
|
+
if (ts.isPropertyAccessExpression(node.expression)) {
|
|
132
|
+
return node.expression.getText(sourceFile);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
|
|
136
|
+
return null; // Anonymous function
|
|
137
|
+
}
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
normalizePath(path) {
|
|
141
|
+
if (!path.startsWith('/')) {
|
|
142
|
+
path = '/' + path;
|
|
143
|
+
}
|
|
144
|
+
path = path.replace(/\/+/g, '/');
|
|
145
|
+
if (path.length > 1 && path.endsWith('/')) {
|
|
146
|
+
path = path.slice(0, -1);
|
|
147
|
+
}
|
|
148
|
+
return path;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
//# sourceMappingURL=koa-discoverer.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { EndpointDiscoverer } from './discoverer-interface.js';
|
|
2
|
+
import { LoadedSourceFile } from '../analysis/source-file-loader.js';
|
|
3
|
+
import { Endpoint } from '../models/endpoint.js';
|
|
4
|
+
export declare class NestJSDiscoverer implements EndpointDiscoverer {
|
|
5
|
+
readonly name = "NestJS";
|
|
6
|
+
private authExtractor;
|
|
7
|
+
constructor();
|
|
8
|
+
discover(file: LoadedSourceFile): Promise<Endpoint[]>;
|
|
9
|
+
private processClass;
|
|
10
|
+
private processMethod;
|
|
11
|
+
private extractControllerPath;
|
|
12
|
+
private extractRoutePath;
|
|
13
|
+
private buildRoute;
|
|
14
|
+
private extractClassAuthInfo;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=nestjs-discoverer.d.ts.map
|