@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,30 @@
|
|
|
1
|
+
import { parseSecurityClassification } from '../core/models/security-classification.js';
|
|
2
|
+
import { parseHttpMethod } from '../core/models/http-method.js';
|
|
3
|
+
import { parseEndpointType } from '../core/models/endpoint-type.js';
|
|
4
|
+
export const defaultScanOptions = {
|
|
5
|
+
output: 'terminal',
|
|
6
|
+
noColor: false,
|
|
7
|
+
noIcons: false,
|
|
8
|
+
};
|
|
9
|
+
export function parseClassificationList(value) {
|
|
10
|
+
return value
|
|
11
|
+
.split(',')
|
|
12
|
+
.map((v) => parseSecurityClassification(v.trim()))
|
|
13
|
+
.filter((v) => v !== undefined);
|
|
14
|
+
}
|
|
15
|
+
export function parseMethodList(value) {
|
|
16
|
+
return value
|
|
17
|
+
.split(',')
|
|
18
|
+
.map((v) => parseHttpMethod(v.trim()))
|
|
19
|
+
.filter((v) => v !== undefined);
|
|
20
|
+
}
|
|
21
|
+
export function parseApiStyleList(value) {
|
|
22
|
+
return value
|
|
23
|
+
.split(',')
|
|
24
|
+
.map((v) => parseEndpointType(v.trim()))
|
|
25
|
+
.filter((v) => v !== undefined);
|
|
26
|
+
}
|
|
27
|
+
export function parseRuleList(value) {
|
|
28
|
+
return value.split(',').map((v) => v.trim().toUpperCase());
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=options.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { ScanResult } from '../models/scan-result.js';
|
|
2
|
+
import { EndpointDiscoverer } from '../discovery/discoverer-interface.js';
|
|
3
|
+
import { SecurityRule } from '../../rules/rule-interface.js';
|
|
4
|
+
export declare class ProjectAnalyzer {
|
|
5
|
+
private sourceLoader;
|
|
6
|
+
private discoverers;
|
|
7
|
+
private rules;
|
|
8
|
+
constructor();
|
|
9
|
+
registerDiscoverer(discoverer: EndpointDiscoverer): void;
|
|
10
|
+
registerRule(rule: SecurityRule): void;
|
|
11
|
+
registerRules(rules: SecurityRule[]): void;
|
|
12
|
+
analyze(projectPath: string): Promise<ScanResult>;
|
|
13
|
+
private discoverEndpoints;
|
|
14
|
+
private evaluateRules;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=project-analyzer.d.ts.map
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { createScanResult } from '../models/scan-result.js';
|
|
2
|
+
import { SourceFileLoader } from './source-file-loader.js';
|
|
3
|
+
export class ProjectAnalyzer {
|
|
4
|
+
sourceLoader;
|
|
5
|
+
discoverers = [];
|
|
6
|
+
rules = [];
|
|
7
|
+
constructor() {
|
|
8
|
+
this.sourceLoader = new SourceFileLoader();
|
|
9
|
+
}
|
|
10
|
+
registerDiscoverer(discoverer) {
|
|
11
|
+
this.discoverers.push(discoverer);
|
|
12
|
+
}
|
|
13
|
+
registerRule(rule) {
|
|
14
|
+
this.rules.push(rule);
|
|
15
|
+
}
|
|
16
|
+
registerRules(rules) {
|
|
17
|
+
this.rules.push(...rules);
|
|
18
|
+
}
|
|
19
|
+
async analyze(projectPath) {
|
|
20
|
+
const startTime = Date.now();
|
|
21
|
+
const sourceFiles = await this.sourceLoader.loadDirectory(projectPath);
|
|
22
|
+
const endpoints = await this.discoverEndpoints(sourceFiles);
|
|
23
|
+
const findings = this.evaluateRules(endpoints);
|
|
24
|
+
const scanDurationMs = Date.now() - startTime;
|
|
25
|
+
return createScanResult({
|
|
26
|
+
projectPath,
|
|
27
|
+
endpoints,
|
|
28
|
+
findings,
|
|
29
|
+
filesScanned: sourceFiles.length,
|
|
30
|
+
scanDurationMs,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
async discoverEndpoints(sourceFiles) {
|
|
34
|
+
const endpoints = [];
|
|
35
|
+
for (const discoverer of this.discoverers) {
|
|
36
|
+
for (const file of sourceFiles) {
|
|
37
|
+
const discovered = await discoverer.discover(file);
|
|
38
|
+
endpoints.push(...discovered);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return endpoints;
|
|
42
|
+
}
|
|
43
|
+
evaluateRules(endpoints) {
|
|
44
|
+
const findings = [];
|
|
45
|
+
for (const rule of this.rules) {
|
|
46
|
+
for (const endpoint of endpoints) {
|
|
47
|
+
const ruleFindings = rule.evaluate(endpoint);
|
|
48
|
+
findings.push(...ruleFindings);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return findings;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=project-analyzer.js.map
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
2
|
+
export interface LoadedSourceFile {
|
|
3
|
+
filePath: string;
|
|
4
|
+
sourceFile: ts.SourceFile;
|
|
5
|
+
content: string;
|
|
6
|
+
}
|
|
7
|
+
export interface SourceFileLoaderOptions {
|
|
8
|
+
extensions?: string[];
|
|
9
|
+
excludePatterns?: string[];
|
|
10
|
+
}
|
|
11
|
+
export declare class SourceFileLoader {
|
|
12
|
+
private options;
|
|
13
|
+
private cache;
|
|
14
|
+
constructor(options?: SourceFileLoaderOptions);
|
|
15
|
+
loadDirectory(dirPath: string): Promise<LoadedSourceFile[]>;
|
|
16
|
+
loadFile(filePath: string): Promise<LoadedSourceFile | null>;
|
|
17
|
+
private getScriptKind;
|
|
18
|
+
clearCache(): void;
|
|
19
|
+
}
|
|
20
|
+
export declare function getLineAndColumn(sourceFile: ts.SourceFile, position: number): {
|
|
21
|
+
line: number;
|
|
22
|
+
column: number;
|
|
23
|
+
};
|
|
24
|
+
export declare function getNodeText(node: ts.Node, sourceFile: ts.SourceFile): string;
|
|
25
|
+
export declare function findNodes<T extends ts.Node>(node: ts.Node, predicate: (node: ts.Node) => node is T): T[];
|
|
26
|
+
export declare function findNodesOfKind<T extends ts.Node>(node: ts.Node, kind: ts.SyntaxKind): T[];
|
|
27
|
+
export declare function getDecorators(node: ts.Node): ts.Decorator[];
|
|
28
|
+
export declare function getDecoratorName(decorator: ts.Decorator): string | null;
|
|
29
|
+
export declare function getDecoratorArguments(decorator: ts.Decorator): ts.Expression[];
|
|
30
|
+
export declare function getStringLiteralValue(node: ts.Node): string | null;
|
|
31
|
+
export declare function getArrayLiteralElements(node: ts.Node): ts.Expression[];
|
|
32
|
+
//# sourceMappingURL=source-file-loader.d.ts.map
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { glob } from 'glob';
|
|
5
|
+
const defaultOptions = {
|
|
6
|
+
extensions: ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'],
|
|
7
|
+
excludePatterns: [
|
|
8
|
+
'**/node_modules/**',
|
|
9
|
+
'**/dist/**',
|
|
10
|
+
'**/build/**',
|
|
11
|
+
'**/.git/**',
|
|
12
|
+
'**/coverage/**',
|
|
13
|
+
'**/*.spec.ts',
|
|
14
|
+
'**/*.test.ts',
|
|
15
|
+
'**/*.spec.js',
|
|
16
|
+
'**/*.test.js',
|
|
17
|
+
],
|
|
18
|
+
};
|
|
19
|
+
export class SourceFileLoader {
|
|
20
|
+
options;
|
|
21
|
+
cache = new Map();
|
|
22
|
+
constructor(options = {}) {
|
|
23
|
+
this.options = { ...defaultOptions, ...options };
|
|
24
|
+
}
|
|
25
|
+
async loadDirectory(dirPath) {
|
|
26
|
+
const absolutePath = path.resolve(dirPath);
|
|
27
|
+
if (!fs.existsSync(absolutePath)) {
|
|
28
|
+
throw new Error(`Directory not found: ${absolutePath}`);
|
|
29
|
+
}
|
|
30
|
+
const patterns = this.options.extensions.map((ext) => `**/*${ext}`);
|
|
31
|
+
const files = [];
|
|
32
|
+
for (const pattern of patterns) {
|
|
33
|
+
const matches = await glob(pattern, {
|
|
34
|
+
cwd: absolutePath,
|
|
35
|
+
ignore: this.options.excludePatterns,
|
|
36
|
+
absolute: true,
|
|
37
|
+
nodir: true,
|
|
38
|
+
});
|
|
39
|
+
files.push(...matches);
|
|
40
|
+
}
|
|
41
|
+
const uniqueFiles = [...new Set(files)];
|
|
42
|
+
const loadedFiles = [];
|
|
43
|
+
for (const filePath of uniqueFiles) {
|
|
44
|
+
try {
|
|
45
|
+
const loaded = await this.loadFile(filePath);
|
|
46
|
+
if (loaded) {
|
|
47
|
+
loadedFiles.push(loaded);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
// Skip files that can't be loaded/parsed
|
|
52
|
+
console.warn(`Warning: Could not load ${filePath}: ${error}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return loadedFiles;
|
|
56
|
+
}
|
|
57
|
+
async loadFile(filePath) {
|
|
58
|
+
const absolutePath = path.resolve(filePath);
|
|
59
|
+
if (this.cache.has(absolutePath)) {
|
|
60
|
+
return this.cache.get(absolutePath);
|
|
61
|
+
}
|
|
62
|
+
if (!fs.existsSync(absolutePath)) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
const content = fs.readFileSync(absolutePath, 'utf-8');
|
|
66
|
+
const sourceFile = ts.createSourceFile(absolutePath, content, ts.ScriptTarget.Latest, true, this.getScriptKind(absolutePath));
|
|
67
|
+
const loaded = {
|
|
68
|
+
filePath: absolutePath,
|
|
69
|
+
sourceFile,
|
|
70
|
+
content,
|
|
71
|
+
};
|
|
72
|
+
this.cache.set(absolutePath, loaded);
|
|
73
|
+
return loaded;
|
|
74
|
+
}
|
|
75
|
+
getScriptKind(filePath) {
|
|
76
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
77
|
+
switch (ext) {
|
|
78
|
+
case '.ts':
|
|
79
|
+
return ts.ScriptKind.TS;
|
|
80
|
+
case '.tsx':
|
|
81
|
+
return ts.ScriptKind.TSX;
|
|
82
|
+
case '.js':
|
|
83
|
+
case '.mjs':
|
|
84
|
+
case '.cjs':
|
|
85
|
+
return ts.ScriptKind.JS;
|
|
86
|
+
case '.jsx':
|
|
87
|
+
return ts.ScriptKind.JSX;
|
|
88
|
+
default:
|
|
89
|
+
return ts.ScriptKind.Unknown;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
clearCache() {
|
|
93
|
+
this.cache.clear();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
export function getLineAndColumn(sourceFile, position) {
|
|
97
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(position);
|
|
98
|
+
return { line: line + 1, column: character + 1 };
|
|
99
|
+
}
|
|
100
|
+
export function getNodeText(node, sourceFile) {
|
|
101
|
+
return node.getText(sourceFile);
|
|
102
|
+
}
|
|
103
|
+
export function findNodes(node, predicate) {
|
|
104
|
+
const results = [];
|
|
105
|
+
function visit(n) {
|
|
106
|
+
if (predicate(n)) {
|
|
107
|
+
results.push(n);
|
|
108
|
+
}
|
|
109
|
+
ts.forEachChild(n, visit);
|
|
110
|
+
}
|
|
111
|
+
visit(node);
|
|
112
|
+
return results;
|
|
113
|
+
}
|
|
114
|
+
export function findNodesOfKind(node, kind) {
|
|
115
|
+
return findNodes(node, (n) => n.kind === kind);
|
|
116
|
+
}
|
|
117
|
+
export function getDecorators(node) {
|
|
118
|
+
if (ts.canHaveDecorators(node)) {
|
|
119
|
+
return (ts.getDecorators(node) ?? []);
|
|
120
|
+
}
|
|
121
|
+
return [];
|
|
122
|
+
}
|
|
123
|
+
export function getDecoratorName(decorator) {
|
|
124
|
+
const expression = decorator.expression;
|
|
125
|
+
if (ts.isIdentifier(expression)) {
|
|
126
|
+
return expression.text;
|
|
127
|
+
}
|
|
128
|
+
if (ts.isCallExpression(expression)) {
|
|
129
|
+
const callee = expression.expression;
|
|
130
|
+
if (ts.isIdentifier(callee)) {
|
|
131
|
+
return callee.text;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
export function getDecoratorArguments(decorator) {
|
|
137
|
+
const expression = decorator.expression;
|
|
138
|
+
if (ts.isCallExpression(expression)) {
|
|
139
|
+
return [...expression.arguments];
|
|
140
|
+
}
|
|
141
|
+
return [];
|
|
142
|
+
}
|
|
143
|
+
export function getStringLiteralValue(node) {
|
|
144
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) {
|
|
145
|
+
return node.text;
|
|
146
|
+
}
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
export function getArrayLiteralElements(node) {
|
|
150
|
+
if (ts.isArrayLiteralExpression(node)) {
|
|
151
|
+
return [...node.elements];
|
|
152
|
+
}
|
|
153
|
+
return [];
|
|
154
|
+
}
|
|
155
|
+
//# sourceMappingURL=source-file-loader.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { AuthorizationInfo } from '../models/authorization-info.js';
|
|
2
|
+
export interface AuthorizationExtractorContext {
|
|
3
|
+
isRouter?: boolean;
|
|
4
|
+
routerMiddlewares?: string[];
|
|
5
|
+
classGuards?: string[];
|
|
6
|
+
classRoles?: string[];
|
|
7
|
+
}
|
|
8
|
+
export interface AuthorizationExtractor {
|
|
9
|
+
extract(middlewares: string[], context?: AuthorizationExtractorContext): AuthorizationInfo;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=authorization-extractor.d.ts.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { AuthorizationInfo } from '../models/authorization-info.js';
|
|
2
|
+
import { AuthorizationExtractor, AuthorizationExtractorContext } from './authorization-extractor.js';
|
|
3
|
+
export declare class ExpressAuthExtractor implements AuthorizationExtractor {
|
|
4
|
+
extract(middlewares: string[], context?: AuthorizationExtractorContext): AuthorizationInfo;
|
|
5
|
+
private processMiddleware;
|
|
6
|
+
private matchesPatterns;
|
|
7
|
+
private containsAuthKeyword;
|
|
8
|
+
private extractRolesFromMiddleware;
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=express-auth-extractor.d.ts.map
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { createDefaultAuthorizationInfo, determineClassification, } from '../models/authorization-info.js';
|
|
2
|
+
// Common authentication middleware patterns
|
|
3
|
+
const AUTH_MIDDLEWARE_PATTERNS = [
|
|
4
|
+
/^passport\.authenticate/i,
|
|
5
|
+
/^jwt$/i,
|
|
6
|
+
/^expressjwt$/i,
|
|
7
|
+
/^requireAuth$/i,
|
|
8
|
+
/^ensureAuthenticated$/i,
|
|
9
|
+
/^isAuthenticated$/i,
|
|
10
|
+
/^authenticate$/i,
|
|
11
|
+
/^authMiddleware$/i,
|
|
12
|
+
/^checkAuth$/i,
|
|
13
|
+
/^verifyToken$/i,
|
|
14
|
+
/^validateToken$/i,
|
|
15
|
+
/^bearerToken$/i,
|
|
16
|
+
/^auth$/i,
|
|
17
|
+
/^protected$/i,
|
|
18
|
+
];
|
|
19
|
+
// Common explicit public/anonymous patterns
|
|
20
|
+
const PUBLIC_PATTERNS = [
|
|
21
|
+
/^allowAnonymous$/i,
|
|
22
|
+
/^public$/i,
|
|
23
|
+
/^skipAuth$/i,
|
|
24
|
+
/^noAuth$/i,
|
|
25
|
+
/^optional$/i,
|
|
26
|
+
];
|
|
27
|
+
// Role-based middleware patterns
|
|
28
|
+
const ROLE_PATTERNS = [
|
|
29
|
+
/^requireRole$/i,
|
|
30
|
+
/^hasRole$/i,
|
|
31
|
+
/^roles$/i,
|
|
32
|
+
/^checkRole$/i,
|
|
33
|
+
/^authorize$/i,
|
|
34
|
+
/^can$/i,
|
|
35
|
+
/^permit$/i,
|
|
36
|
+
];
|
|
37
|
+
export class ExpressAuthExtractor {
|
|
38
|
+
extract(middlewares, context) {
|
|
39
|
+
const allMiddlewares = [
|
|
40
|
+
...(context?.routerMiddlewares ?? []),
|
|
41
|
+
...middlewares,
|
|
42
|
+
];
|
|
43
|
+
const auth = createDefaultAuthorizationInfo();
|
|
44
|
+
auth.middlewareChain = allMiddlewares;
|
|
45
|
+
for (const middleware of allMiddlewares) {
|
|
46
|
+
this.processMiddleware(middleware, auth);
|
|
47
|
+
}
|
|
48
|
+
auth.classification = determineClassification(auth);
|
|
49
|
+
return auth;
|
|
50
|
+
}
|
|
51
|
+
processMiddleware(middleware, auth) {
|
|
52
|
+
const normalizedName = middleware.trim();
|
|
53
|
+
// Check for explicit public markers
|
|
54
|
+
if (this.matchesPatterns(normalizedName, PUBLIC_PATTERNS)) {
|
|
55
|
+
auth.isExplicitlyPublic = true;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
// Check for authentication middleware
|
|
59
|
+
if (this.matchesPatterns(normalizedName, AUTH_MIDDLEWARE_PATTERNS)) {
|
|
60
|
+
auth.isAuthenticated = true;
|
|
61
|
+
auth.guardNames.push(normalizedName);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
// Check for role-based middleware
|
|
65
|
+
if (this.matchesPatterns(normalizedName, ROLE_PATTERNS)) {
|
|
66
|
+
auth.isAuthenticated = true;
|
|
67
|
+
// Try to extract role names from common patterns
|
|
68
|
+
const roles = this.extractRolesFromMiddleware(normalizedName);
|
|
69
|
+
auth.roles.push(...roles);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
// Check for common auth patterns in the middleware name
|
|
73
|
+
if (this.containsAuthKeyword(normalizedName)) {
|
|
74
|
+
auth.isAuthenticated = true;
|
|
75
|
+
auth.guardNames.push(normalizedName);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
matchesPatterns(name, patterns) {
|
|
79
|
+
// Get just the function/method name (strip object prefix)
|
|
80
|
+
const funcName = name.includes('.') ? name.split('.').pop() : name;
|
|
81
|
+
return patterns.some((pattern) => pattern.test(funcName));
|
|
82
|
+
}
|
|
83
|
+
containsAuthKeyword(name) {
|
|
84
|
+
const keywords = ['auth', 'jwt', 'token', 'session', 'login', 'secure'];
|
|
85
|
+
const lower = name.toLowerCase();
|
|
86
|
+
return keywords.some((kw) => lower.includes(kw));
|
|
87
|
+
}
|
|
88
|
+
extractRolesFromMiddleware(middleware) {
|
|
89
|
+
// Try to extract roles from patterns like:
|
|
90
|
+
// requireRole('admin'), hasRole(['user', 'admin']), roles('manager')
|
|
91
|
+
const roleMatch = middleware.match(/\(['"]?([^'")\]]+)['"]?\]/i);
|
|
92
|
+
if (roleMatch) {
|
|
93
|
+
return roleMatch[1].split(',').map((r) => r.trim().replace(/['"]/g, ''));
|
|
94
|
+
}
|
|
95
|
+
// For patterns like authorize.admin or can.read
|
|
96
|
+
if (middleware.includes('.')) {
|
|
97
|
+
const parts = middleware.split('.');
|
|
98
|
+
const lastPart = parts[parts.length - 1];
|
|
99
|
+
if (!this.matchesPatterns(lastPart, AUTH_MIDDLEWARE_PATTERNS)) {
|
|
100
|
+
return [lastPart];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=express-auth-extractor.js.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { LoadedSourceFile } from '../analysis/source-file-loader.js';
|
|
2
|
+
export interface GlobalAuthConfig {
|
|
3
|
+
hasGlobalGuard: boolean;
|
|
4
|
+
globalGuardName?: string;
|
|
5
|
+
hasGlobalPrefix: boolean;
|
|
6
|
+
globalPrefix?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare class GlobalAuthAnalyzer {
|
|
9
|
+
analyze(files: LoadedSourceFile[]): GlobalAuthConfig;
|
|
10
|
+
private analyzeFile;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=global-auth-analyzer.d.ts.map
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
2
|
+
import { findNodes } from '../analysis/source-file-loader.js';
|
|
3
|
+
export class GlobalAuthAnalyzer {
|
|
4
|
+
analyze(files) {
|
|
5
|
+
const config = {
|
|
6
|
+
hasGlobalGuard: false,
|
|
7
|
+
hasGlobalPrefix: false,
|
|
8
|
+
};
|
|
9
|
+
for (const file of files) {
|
|
10
|
+
this.analyzeFile(file, config);
|
|
11
|
+
}
|
|
12
|
+
return config;
|
|
13
|
+
}
|
|
14
|
+
analyzeFile(file, config) {
|
|
15
|
+
const { sourceFile } = file;
|
|
16
|
+
// Look for app.useGlobalGuards() calls
|
|
17
|
+
const callExpressions = findNodes(sourceFile, ts.isCallExpression);
|
|
18
|
+
for (const callExpr of callExpressions) {
|
|
19
|
+
// Check for app.useGlobalGuards(new AuthGuard())
|
|
20
|
+
if (ts.isPropertyAccessExpression(callExpr.expression)) {
|
|
21
|
+
const propAccess = callExpr.expression;
|
|
22
|
+
const methodName = propAccess.name.text;
|
|
23
|
+
if (methodName === 'useGlobalGuards') {
|
|
24
|
+
config.hasGlobalGuard = true;
|
|
25
|
+
// Try to extract guard name
|
|
26
|
+
if (callExpr.arguments.length > 0) {
|
|
27
|
+
const arg = callExpr.arguments[0];
|
|
28
|
+
if (ts.isNewExpression(arg) && ts.isIdentifier(arg.expression)) {
|
|
29
|
+
config.globalGuardName = arg.expression.text;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (methodName === 'setGlobalPrefix') {
|
|
34
|
+
config.hasGlobalPrefix = true;
|
|
35
|
+
if (callExpr.arguments.length > 0) {
|
|
36
|
+
const arg = callExpr.arguments[0];
|
|
37
|
+
if (ts.isStringLiteral(arg)) {
|
|
38
|
+
config.globalPrefix = arg.text;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// Check for APP_GUARD provider pattern
|
|
44
|
+
// { provide: APP_GUARD, useClass: AuthGuard }
|
|
45
|
+
if (callExpr.arguments.length > 0 &&
|
|
46
|
+
ts.isObjectLiteralExpression(callExpr.arguments[0])) {
|
|
47
|
+
const objLiteral = callExpr.arguments[0];
|
|
48
|
+
let hasAppGuard = false;
|
|
49
|
+
let guardName;
|
|
50
|
+
for (const prop of objLiteral.properties) {
|
|
51
|
+
if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
|
|
52
|
+
if (prop.name.text === 'provide') {
|
|
53
|
+
if (ts.isIdentifier(prop.initializer)) {
|
|
54
|
+
if (prop.initializer.text === 'APP_GUARD') {
|
|
55
|
+
hasAppGuard = true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (prop.name.text === 'useClass') {
|
|
60
|
+
if (ts.isIdentifier(prop.initializer)) {
|
|
61
|
+
guardName = prop.initializer.text;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
if (hasAppGuard && guardName) {
|
|
67
|
+
config.hasGlobalGuard = true;
|
|
68
|
+
config.globalGuardName = guardName;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
//# sourceMappingURL=global-auth-analyzer.js.map
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
2
|
+
import { AuthorizationInfo } from '../models/authorization-info.js';
|
|
3
|
+
export interface NestJSAuthContext {
|
|
4
|
+
classGuards?: string[];
|
|
5
|
+
classRoles?: string[];
|
|
6
|
+
isPublic?: boolean;
|
|
7
|
+
}
|
|
8
|
+
export declare class NestJSAuthExtractor {
|
|
9
|
+
extract(decorators: ts.Decorator[], context?: NestJSAuthContext): AuthorizationInfo;
|
|
10
|
+
private processDecorator;
|
|
11
|
+
private isAuthGuard;
|
|
12
|
+
}
|
|
13
|
+
//# sourceMappingURL=nestjs-auth-extractor.d.ts.map
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
2
|
+
import { createDefaultAuthorizationInfo, determineClassification, } from '../models/authorization-info.js';
|
|
3
|
+
import { getDecoratorName, getDecoratorArguments, getStringLiteralValue, getArrayLiteralElements, } from '../analysis/source-file-loader.js';
|
|
4
|
+
// Auth guard patterns
|
|
5
|
+
const AUTH_GUARD_PATTERNS = [
|
|
6
|
+
/AuthGuard/i,
|
|
7
|
+
/JwtAuthGuard/i,
|
|
8
|
+
/LocalAuthGuard/i,
|
|
9
|
+
/SessionGuard/i,
|
|
10
|
+
/BearerGuard/i,
|
|
11
|
+
/TokenGuard/i,
|
|
12
|
+
];
|
|
13
|
+
// Public decorator names
|
|
14
|
+
const PUBLIC_DECORATORS = new Set([
|
|
15
|
+
'Public',
|
|
16
|
+
'AllowAnonymous',
|
|
17
|
+
'SkipAuth',
|
|
18
|
+
'NoAuth',
|
|
19
|
+
'IsPublic',
|
|
20
|
+
]);
|
|
21
|
+
// Role decorator names
|
|
22
|
+
const ROLE_DECORATORS = new Set(['Roles', 'RequireRoles', 'HasRoles', 'Authorize']);
|
|
23
|
+
// Policy decorator names
|
|
24
|
+
const POLICY_DECORATORS = new Set(['Policies', 'RequirePolicies', 'CheckPolicies']);
|
|
25
|
+
export class NestJSAuthExtractor {
|
|
26
|
+
extract(decorators, context) {
|
|
27
|
+
const auth = createDefaultAuthorizationInfo();
|
|
28
|
+
// Apply class-level auth first
|
|
29
|
+
if (context?.classGuards) {
|
|
30
|
+
for (const guard of context.classGuards) {
|
|
31
|
+
if (this.isAuthGuard(guard)) {
|
|
32
|
+
auth.isAuthenticated = true;
|
|
33
|
+
auth.guardNames.push(guard);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
if (context?.classRoles) {
|
|
38
|
+
auth.roles.push(...context.classRoles);
|
|
39
|
+
if (context.classRoles.length > 0) {
|
|
40
|
+
auth.isAuthenticated = true;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
if (context?.isPublic) {
|
|
44
|
+
auth.isExplicitlyPublic = true;
|
|
45
|
+
}
|
|
46
|
+
// Process method-level decorators (can override class-level)
|
|
47
|
+
for (const decorator of decorators) {
|
|
48
|
+
this.processDecorator(decorator, auth);
|
|
49
|
+
}
|
|
50
|
+
// Method-level @Public overrides class-level guards
|
|
51
|
+
const hasMethodPublic = decorators.some((d) => {
|
|
52
|
+
const name = getDecoratorName(d);
|
|
53
|
+
return name && PUBLIC_DECORATORS.has(name);
|
|
54
|
+
});
|
|
55
|
+
if (hasMethodPublic) {
|
|
56
|
+
auth.isExplicitlyPublic = true;
|
|
57
|
+
// Don't clear isAuthenticated as it creates the AP003 conflict scenario
|
|
58
|
+
}
|
|
59
|
+
auth.classification = determineClassification(auth);
|
|
60
|
+
return auth;
|
|
61
|
+
}
|
|
62
|
+
processDecorator(decorator, auth) {
|
|
63
|
+
const name = getDecoratorName(decorator);
|
|
64
|
+
if (!name)
|
|
65
|
+
return;
|
|
66
|
+
// Check for @Public or similar
|
|
67
|
+
if (PUBLIC_DECORATORS.has(name)) {
|
|
68
|
+
auth.isExplicitlyPublic = true;
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
// Check for @UseGuards
|
|
72
|
+
if (name === 'UseGuards') {
|
|
73
|
+
const args = getDecoratorArguments(decorator);
|
|
74
|
+
for (const arg of args) {
|
|
75
|
+
if (ts.isIdentifier(arg)) {
|
|
76
|
+
const guardName = arg.text;
|
|
77
|
+
if (this.isAuthGuard(guardName)) {
|
|
78
|
+
auth.isAuthenticated = true;
|
|
79
|
+
}
|
|
80
|
+
auth.guardNames.push(guardName);
|
|
81
|
+
}
|
|
82
|
+
// Handle UseGuards(AuthGuard('jwt'))
|
|
83
|
+
if (ts.isCallExpression(arg)) {
|
|
84
|
+
if (ts.isIdentifier(arg.expression)) {
|
|
85
|
+
const guardName = arg.expression.text;
|
|
86
|
+
if (this.isAuthGuard(guardName)) {
|
|
87
|
+
auth.isAuthenticated = true;
|
|
88
|
+
}
|
|
89
|
+
auth.guardNames.push(guardName);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
// Check for @Roles
|
|
96
|
+
if (ROLE_DECORATORS.has(name)) {
|
|
97
|
+
const args = getDecoratorArguments(decorator);
|
|
98
|
+
for (const arg of args) {
|
|
99
|
+
// @Roles('admin', 'user')
|
|
100
|
+
const value = getStringLiteralValue(arg);
|
|
101
|
+
if (value) {
|
|
102
|
+
auth.roles.push(value);
|
|
103
|
+
auth.isAuthenticated = true;
|
|
104
|
+
}
|
|
105
|
+
// @Roles(['admin', 'user'])
|
|
106
|
+
const arrayElements = getArrayLiteralElements(arg);
|
|
107
|
+
for (const elem of arrayElements) {
|
|
108
|
+
const elemValue = getStringLiteralValue(elem);
|
|
109
|
+
if (elemValue) {
|
|
110
|
+
auth.roles.push(elemValue);
|
|
111
|
+
auth.isAuthenticated = true;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
// Check for @Policies
|
|
118
|
+
if (POLICY_DECORATORS.has(name)) {
|
|
119
|
+
const args = getDecoratorArguments(decorator);
|
|
120
|
+
for (const arg of args) {
|
|
121
|
+
const value = getStringLiteralValue(arg);
|
|
122
|
+
if (value) {
|
|
123
|
+
auth.policies.push(value);
|
|
124
|
+
auth.isAuthenticated = true;
|
|
125
|
+
}
|
|
126
|
+
const arrayElements = getArrayLiteralElements(arg);
|
|
127
|
+
for (const elem of arrayElements) {
|
|
128
|
+
const elemValue = getStringLiteralValue(elem);
|
|
129
|
+
if (elemValue) {
|
|
130
|
+
auth.policies.push(elemValue);
|
|
131
|
+
auth.isAuthenticated = true;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
isAuthGuard(guardName) {
|
|
139
|
+
return AUTH_GUARD_PATTERNS.some((pattern) => pattern.test(guardName));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=nestjs-auth-extractor.js.map
|