@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,27 @@
|
|
|
1
|
+
import { RuleConfig } from '../../rules/rule-interface.js';
|
|
2
|
+
export interface ApiPostureConfig {
|
|
3
|
+
rules?: Record<string, RuleConfig>;
|
|
4
|
+
suppressions?: SuppressionConfig[];
|
|
5
|
+
output?: {
|
|
6
|
+
format?: 'terminal' | 'json' | 'markdown';
|
|
7
|
+
noColor?: boolean;
|
|
8
|
+
noIcons?: boolean;
|
|
9
|
+
};
|
|
10
|
+
scan?: {
|
|
11
|
+
excludePatterns?: string[];
|
|
12
|
+
includePatterns?: string[];
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
export interface SuppressionConfig {
|
|
16
|
+
ruleId?: string;
|
|
17
|
+
route?: string;
|
|
18
|
+
routePattern?: string;
|
|
19
|
+
method?: string;
|
|
20
|
+
reason: string;
|
|
21
|
+
}
|
|
22
|
+
export declare class ConfigLoader {
|
|
23
|
+
load(configPath?: string): Promise<ApiPostureConfig>;
|
|
24
|
+
private findConfigFile;
|
|
25
|
+
private validateConfig;
|
|
26
|
+
}
|
|
27
|
+
//# sourceMappingURL=config-loader.d.ts.map
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { parseSeverity } from '../models/severity.js';
|
|
4
|
+
const CONFIG_FILE_NAMES = [
|
|
5
|
+
'.apiposture.json',
|
|
6
|
+
'apiposture.json',
|
|
7
|
+
'.apiposture.config.json',
|
|
8
|
+
];
|
|
9
|
+
export class ConfigLoader {
|
|
10
|
+
async load(configPath) {
|
|
11
|
+
let filePath = configPath ?? null;
|
|
12
|
+
// If no path specified, search for config file
|
|
13
|
+
if (!filePath) {
|
|
14
|
+
filePath = this.findConfigFile(process.cwd());
|
|
15
|
+
}
|
|
16
|
+
if (!filePath) {
|
|
17
|
+
return {};
|
|
18
|
+
}
|
|
19
|
+
try {
|
|
20
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
21
|
+
const config = JSON.parse(content);
|
|
22
|
+
return this.validateConfig(config);
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
if (configPath) {
|
|
26
|
+
// Only throw if user explicitly specified a config path
|
|
27
|
+
throw new Error(`Failed to load config from ${filePath}: ${error}`);
|
|
28
|
+
}
|
|
29
|
+
return {};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
findConfigFile(startDir) {
|
|
33
|
+
let currentDir = startDir;
|
|
34
|
+
let parentDir = path.dirname(currentDir);
|
|
35
|
+
while (currentDir !== parentDir) {
|
|
36
|
+
for (const fileName of CONFIG_FILE_NAMES) {
|
|
37
|
+
const filePath = path.join(currentDir, fileName);
|
|
38
|
+
if (fs.existsSync(filePath)) {
|
|
39
|
+
return filePath;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
currentDir = parentDir;
|
|
43
|
+
parentDir = path.dirname(currentDir);
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
validateConfig(config) {
|
|
48
|
+
// Validate rules config
|
|
49
|
+
if (config.rules) {
|
|
50
|
+
for (const [ruleId, ruleConfig] of Object.entries(config.rules)) {
|
|
51
|
+
if (ruleConfig.severity) {
|
|
52
|
+
const parsed = parseSeverity(ruleConfig.severity);
|
|
53
|
+
if (!parsed) {
|
|
54
|
+
console.warn(`Invalid severity "${ruleConfig.severity}" for rule ${ruleId}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// Validate suppressions
|
|
60
|
+
if (config.suppressions) {
|
|
61
|
+
config.suppressions = config.suppressions.filter((s) => {
|
|
62
|
+
if (!s.reason) {
|
|
63
|
+
console.warn('Suppression missing required "reason" field, skipping');
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
return true;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
return config;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=config-loader.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Finding } from '../models/finding.js';
|
|
2
|
+
import { Endpoint } from '../models/endpoint.js';
|
|
3
|
+
import { SuppressionConfig } from './config-loader.js';
|
|
4
|
+
export declare class SuppressionMatcher {
|
|
5
|
+
private suppressions;
|
|
6
|
+
constructor(suppressions?: SuppressionConfig[]);
|
|
7
|
+
applySuppressionsToFindings(findings: Finding[]): Finding[];
|
|
8
|
+
isEndpointSuppressed(endpoint: Endpoint, ruleId: string): SuppressionConfig | null;
|
|
9
|
+
private findMatchingSuppression;
|
|
10
|
+
private matchesFinding;
|
|
11
|
+
private matchesEndpoint;
|
|
12
|
+
private matchesRoutePattern;
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=suppression-matcher.d.ts.map
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
export class SuppressionMatcher {
|
|
2
|
+
suppressions;
|
|
3
|
+
constructor(suppressions = []) {
|
|
4
|
+
this.suppressions = suppressions;
|
|
5
|
+
}
|
|
6
|
+
applySuppressionsToFindings(findings) {
|
|
7
|
+
return findings.map((finding) => {
|
|
8
|
+
const suppression = this.findMatchingSuppression(finding);
|
|
9
|
+
if (suppression) {
|
|
10
|
+
return {
|
|
11
|
+
...finding,
|
|
12
|
+
suppressed: true,
|
|
13
|
+
suppressionReason: suppression.reason,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
return finding;
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
isEndpointSuppressed(endpoint, ruleId) {
|
|
20
|
+
for (const suppression of this.suppressions) {
|
|
21
|
+
if (this.matchesEndpoint(suppression, endpoint, ruleId)) {
|
|
22
|
+
return suppression;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
findMatchingSuppression(finding) {
|
|
28
|
+
for (const suppression of this.suppressions) {
|
|
29
|
+
if (this.matchesFinding(suppression, finding)) {
|
|
30
|
+
return suppression;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
matchesFinding(suppression, finding) {
|
|
36
|
+
return this.matchesEndpoint(suppression, finding.endpoint, finding.ruleId);
|
|
37
|
+
}
|
|
38
|
+
matchesEndpoint(suppression, endpoint, ruleId) {
|
|
39
|
+
// Check rule ID
|
|
40
|
+
if (suppression.ruleId && suppression.ruleId !== ruleId) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
// Check HTTP method
|
|
44
|
+
if (suppression.method && suppression.method.toUpperCase() !== endpoint.method) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
// Check exact route match
|
|
48
|
+
if (suppression.route && suppression.route !== endpoint.route) {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
// Check route pattern (regex or glob-like)
|
|
52
|
+
if (suppression.routePattern) {
|
|
53
|
+
if (!this.matchesRoutePattern(suppression.routePattern, endpoint.route)) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
matchesRoutePattern(pattern, route) {
|
|
60
|
+
// Convert glob-like pattern to regex
|
|
61
|
+
// * matches any segment, ** matches any path
|
|
62
|
+
let regexPattern = pattern
|
|
63
|
+
.replace(/\*\*/g, '<<<DOUBLE_STAR>>>')
|
|
64
|
+
.replace(/\*/g, '[^/]+')
|
|
65
|
+
.replace(/<<<DOUBLE_STAR>>>/g, '.*');
|
|
66
|
+
// Escape regex special chars except those we converted
|
|
67
|
+
regexPattern = regexPattern.replace(/[.+^${}()|[\]\\]/g, '\\$&');
|
|
68
|
+
// Anchor the pattern
|
|
69
|
+
regexPattern = `^${regexPattern}$`;
|
|
70
|
+
try {
|
|
71
|
+
const regex = new RegExp(regexPattern);
|
|
72
|
+
return regex.test(route);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=suppression-matcher.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { Endpoint } from '../models/endpoint.js';
|
|
2
|
+
import { LoadedSourceFile } from '../analysis/source-file-loader.js';
|
|
3
|
+
export interface EndpointDiscoverer {
|
|
4
|
+
readonly name: string;
|
|
5
|
+
discover(file: LoadedSourceFile): Promise<Endpoint[]>;
|
|
6
|
+
}
|
|
7
|
+
//# sourceMappingURL=discoverer-interface.d.ts.map
|
|
@@ -0,0 +1,20 @@
|
|
|
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 ExpressDiscoverer implements EndpointDiscoverer {
|
|
5
|
+
readonly name = "Express.js";
|
|
6
|
+
private registry;
|
|
7
|
+
private authExtractor;
|
|
8
|
+
constructor();
|
|
9
|
+
discover(file: LoadedSourceFile): Promise<Endpoint[]>;
|
|
10
|
+
private collectRouterMounts;
|
|
11
|
+
private processCallExpression;
|
|
12
|
+
private getCallerName;
|
|
13
|
+
private isExpressIdentifier;
|
|
14
|
+
private extractRoutePath;
|
|
15
|
+
private extractMiddlewares;
|
|
16
|
+
private extractMiddlewareName;
|
|
17
|
+
private extractHandlerName;
|
|
18
|
+
private normalizePath;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=express-discoverer.d.ts.map
|
|
@@ -0,0 +1,223 @@
|
|
|
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 { RouteGroupRegistry } from './route-group-registry.js';
|
|
7
|
+
import { ExpressAuthExtractor } from '../authorization/express-auth-extractor.js';
|
|
8
|
+
const EXPRESS_HTTP_METHODS = new Set([
|
|
9
|
+
'get', 'post', 'put', 'delete', 'patch', 'options', 'head', 'all'
|
|
10
|
+
]);
|
|
11
|
+
const EXPRESS_IDENTIFIERS = new Set(['app', 'router', 'express']);
|
|
12
|
+
export class ExpressDiscoverer {
|
|
13
|
+
name = 'Express.js';
|
|
14
|
+
registry;
|
|
15
|
+
authExtractor;
|
|
16
|
+
constructor() {
|
|
17
|
+
this.registry = new RouteGroupRegistry();
|
|
18
|
+
this.authExtractor = new ExpressAuthExtractor();
|
|
19
|
+
}
|
|
20
|
+
async discover(file) {
|
|
21
|
+
const endpoints = [];
|
|
22
|
+
const { sourceFile, filePath } = file;
|
|
23
|
+
// First pass: collect router mounts and route groups
|
|
24
|
+
this.collectRouterMounts(sourceFile, filePath);
|
|
25
|
+
// Second pass: find route definitions
|
|
26
|
+
const callExpressions = findNodes(sourceFile, ts.isCallExpression);
|
|
27
|
+
for (const callExpr of callExpressions) {
|
|
28
|
+
const endpoint = this.processCallExpression(callExpr, sourceFile, filePath);
|
|
29
|
+
if (endpoint) {
|
|
30
|
+
endpoints.push(endpoint);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return endpoints;
|
|
34
|
+
}
|
|
35
|
+
collectRouterMounts(sourceFile, filePath) {
|
|
36
|
+
const callExpressions = findNodes(sourceFile, ts.isCallExpression);
|
|
37
|
+
for (const callExpr of callExpressions) {
|
|
38
|
+
// Look for app.use('/prefix', router)
|
|
39
|
+
if (!ts.isPropertyAccessExpression(callExpr.expression))
|
|
40
|
+
continue;
|
|
41
|
+
const propAccess = callExpr.expression;
|
|
42
|
+
const methodName = propAccess.name.text;
|
|
43
|
+
if (methodName !== 'use')
|
|
44
|
+
continue;
|
|
45
|
+
const args = callExpr.arguments;
|
|
46
|
+
if (args.length < 2)
|
|
47
|
+
continue;
|
|
48
|
+
const firstArg = args[0];
|
|
49
|
+
const secondArg = args[1];
|
|
50
|
+
// Check for app.use('/prefix', router)
|
|
51
|
+
if (ts.isStringLiteral(firstArg) && ts.isIdentifier(secondArg)) {
|
|
52
|
+
const prefix = firstArg.text;
|
|
53
|
+
const routerName = secondArg.text;
|
|
54
|
+
const appName = ts.isIdentifier(propAccess.expression)
|
|
55
|
+
? propAccess.expression.text
|
|
56
|
+
: '';
|
|
57
|
+
if (appName) {
|
|
58
|
+
this.registry.registerRouterMount(filePath, appName, prefix, routerName);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
processCallExpression(callExpr, sourceFile, filePath) {
|
|
64
|
+
// Check for pattern: app.get('/path', handler) or router.post('/path', handler)
|
|
65
|
+
if (!ts.isPropertyAccessExpression(callExpr.expression)) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
const propAccess = callExpr.expression;
|
|
69
|
+
const methodName = propAccess.name.text.toLowerCase();
|
|
70
|
+
// Check if this is an HTTP method call
|
|
71
|
+
if (!EXPRESS_HTTP_METHODS.has(methodName)) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
// Check if caller is an Express identifier
|
|
75
|
+
const callerName = this.getCallerName(propAccess.expression);
|
|
76
|
+
if (!callerName || !this.isExpressIdentifier(callerName)) {
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
// Get route path from first argument
|
|
80
|
+
const args = callExpr.arguments;
|
|
81
|
+
if (args.length === 0) {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
const routePath = this.extractRoutePath(args[0]);
|
|
85
|
+
if (!routePath) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
// Get prefix if this is a router
|
|
89
|
+
const prefix = this.registry.getRouterPrefix(filePath, callerName);
|
|
90
|
+
const fullRoute = this.normalizePath(prefix + routePath);
|
|
91
|
+
// Extract middleware chain (all arguments except last handler)
|
|
92
|
+
const middlewares = this.extractMiddlewares(args, sourceFile);
|
|
93
|
+
// Get handler name
|
|
94
|
+
const handlerName = this.extractHandlerName(args[args.length - 1], sourceFile);
|
|
95
|
+
// Get location
|
|
96
|
+
const location = getLineAndColumn(sourceFile, callExpr.getStart(sourceFile));
|
|
97
|
+
// Extract authorization info
|
|
98
|
+
const authorization = this.authExtractor.extract(middlewares, {
|
|
99
|
+
isRouter: callerName !== 'app',
|
|
100
|
+
routerMiddlewares: this.registry.getAllMiddlewares(filePath, callerName),
|
|
101
|
+
});
|
|
102
|
+
return createEndpoint({
|
|
103
|
+
route: fullRoute,
|
|
104
|
+
method: parseHttpMethod(methodName.toUpperCase()) ?? HttpMethod.GET,
|
|
105
|
+
handlerName,
|
|
106
|
+
type: EndpointType.Express,
|
|
107
|
+
location: {
|
|
108
|
+
filePath,
|
|
109
|
+
line: location.line,
|
|
110
|
+
column: location.column,
|
|
111
|
+
},
|
|
112
|
+
authorization,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
getCallerName(expression) {
|
|
116
|
+
if (ts.isIdentifier(expression)) {
|
|
117
|
+
return expression.text;
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
isExpressIdentifier(name) {
|
|
122
|
+
// Common Express variable names
|
|
123
|
+
if (EXPRESS_IDENTIFIERS.has(name)) {
|
|
124
|
+
return true;
|
|
125
|
+
}
|
|
126
|
+
// Also match common patterns like userRouter, apiRouter, etc.
|
|
127
|
+
if (name.toLowerCase().includes('router') || name.toLowerCase().includes('app')) {
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
extractRoutePath(node) {
|
|
133
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
|
|
134
|
+
return node.text;
|
|
135
|
+
}
|
|
136
|
+
// Handle template literals with embedded expressions (extract just the static parts)
|
|
137
|
+
if (ts.isTemplateExpression(node)) {
|
|
138
|
+
let path = node.head.text;
|
|
139
|
+
for (const span of node.templateSpans) {
|
|
140
|
+
path += ':param' + span.literal.text;
|
|
141
|
+
}
|
|
142
|
+
return path;
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
146
|
+
extractMiddlewares(args, sourceFile) {
|
|
147
|
+
const middlewares = [];
|
|
148
|
+
// All arguments except possibly the last one (the main handler) could be middleware
|
|
149
|
+
for (let i = 0; i < args.length - 1; i++) {
|
|
150
|
+
const arg = args[i];
|
|
151
|
+
// Skip the route path (first arg if it's a string)
|
|
152
|
+
if (i === 0 && (ts.isStringLiteral(arg) || ts.isTemplateExpression(arg))) {
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
const middlewareName = this.extractMiddlewareName(arg, sourceFile);
|
|
156
|
+
if (middlewareName) {
|
|
157
|
+
middlewares.push(middlewareName);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return middlewares;
|
|
161
|
+
}
|
|
162
|
+
extractMiddlewareName(node, sourceFile) {
|
|
163
|
+
// Direct identifier: requireAuth
|
|
164
|
+
if (ts.isIdentifier(node)) {
|
|
165
|
+
return node.text;
|
|
166
|
+
}
|
|
167
|
+
// Call expression: passport.authenticate('jwt')
|
|
168
|
+
if (ts.isCallExpression(node)) {
|
|
169
|
+
if (ts.isPropertyAccessExpression(node.expression)) {
|
|
170
|
+
const obj = node.expression.expression;
|
|
171
|
+
const method = node.expression.name;
|
|
172
|
+
if (ts.isIdentifier(obj) && ts.isIdentifier(method)) {
|
|
173
|
+
return `${obj.text}.${method.text}`;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (ts.isIdentifier(node.expression)) {
|
|
177
|
+
return node.expression.text;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// Property access: auth.requireRole
|
|
181
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
182
|
+
const text = node.getText(sourceFile);
|
|
183
|
+
return text;
|
|
184
|
+
}
|
|
185
|
+
// Array of middleware: [auth, validate]
|
|
186
|
+
if (ts.isArrayLiteralExpression(node)) {
|
|
187
|
+
return node.elements
|
|
188
|
+
.map((el) => this.extractMiddlewareName(el, sourceFile))
|
|
189
|
+
.filter(Boolean)
|
|
190
|
+
.join(',');
|
|
191
|
+
}
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
extractHandlerName(node, sourceFile) {
|
|
195
|
+
// Direct identifier
|
|
196
|
+
if (ts.isIdentifier(node)) {
|
|
197
|
+
return node.text;
|
|
198
|
+
}
|
|
199
|
+
// Property access: controller.method
|
|
200
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
201
|
+
return node.getText(sourceFile);
|
|
202
|
+
}
|
|
203
|
+
// Arrow function or function expression
|
|
204
|
+
if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
|
|
205
|
+
return 'anonymous';
|
|
206
|
+
}
|
|
207
|
+
return 'unknown';
|
|
208
|
+
}
|
|
209
|
+
normalizePath(path) {
|
|
210
|
+
// Ensure path starts with /
|
|
211
|
+
if (!path.startsWith('/')) {
|
|
212
|
+
path = '/' + path;
|
|
213
|
+
}
|
|
214
|
+
// Remove duplicate slashes
|
|
215
|
+
path = path.replace(/\/+/g, '/');
|
|
216
|
+
// Remove trailing slash (except for root)
|
|
217
|
+
if (path.length > 1 && path.endsWith('/')) {
|
|
218
|
+
path = path.slice(0, -1);
|
|
219
|
+
}
|
|
220
|
+
return path;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
//# sourceMappingURL=express-discoverer.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
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 FastifyDiscoverer implements EndpointDiscoverer {
|
|
5
|
+
readonly name = "Fastify";
|
|
6
|
+
discover(file: LoadedSourceFile): Promise<Endpoint[]>;
|
|
7
|
+
private processShorthandRoute;
|
|
8
|
+
private processRouteMethod;
|
|
9
|
+
private extractAuthFromOptions;
|
|
10
|
+
private extractHooksAuth;
|
|
11
|
+
private extractHookName;
|
|
12
|
+
private isAuthHook;
|
|
13
|
+
private getCallerName;
|
|
14
|
+
private isFastifyIdentifier;
|
|
15
|
+
private extractStringValue;
|
|
16
|
+
private extractHandlerName;
|
|
17
|
+
private normalizePath;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=fastify-discoverer.d.ts.map
|