pumuki 6.3.293 → 6.3.295
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/CHANGELOG.md +4 -0
- package/VERSION +1 -1
- package/core/facts/detectors/text/ios.test.ts +65 -0
- package/core/facts/detectors/text/ios.ts +66 -6
- package/core/facts/detectors/typescript/index.test.ts +1115 -94
- package/core/facts/detectors/typescript/index.ts +653 -1
- package/core/facts/extractHeuristicFacts.ts +33 -2
- package/core/rules/presets/heuristics/ios.test.ts +11 -1
- package/core/rules/presets/heuristics/ios.ts +36 -0
- package/core/rules/presets/heuristics/typescript.test.ts +73 -1
- package/core/rules/presets/heuristics/typescript.ts +264 -0
- package/integrations/config/skillsDetectorRegistry.ts +90 -0
- package/integrations/git/runPlatformGate.ts +41 -3
- package/package.json +1 -1
|
@@ -1910,6 +1910,149 @@ const objectExpressionPropertyNames = (node: AstNode): Set<string> => {
|
|
|
1910
1910
|
);
|
|
1911
1911
|
};
|
|
1912
1912
|
|
|
1913
|
+
const hasPropertyMatching = (propertyNames: ReadonlySet<string>, pattern: RegExp): boolean => {
|
|
1914
|
+
for (const propertyName of propertyNames) {
|
|
1915
|
+
if (pattern.test(propertyName)) {
|
|
1916
|
+
return true;
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
return false;
|
|
1920
|
+
};
|
|
1921
|
+
|
|
1922
|
+
const backendAccessTokenPropertyPattern = /^(accessToken|access_token)$/;
|
|
1923
|
+
const backendRefreshTokenPropertyPattern = /^(refreshToken|refresh_token)$/;
|
|
1924
|
+
|
|
1925
|
+
const isBackendAuthResponseObjectWithoutRefreshToken = (node: AstNode): boolean => {
|
|
1926
|
+
if (node.type !== 'ObjectExpression') {
|
|
1927
|
+
return false;
|
|
1928
|
+
}
|
|
1929
|
+
const propertyNames = objectExpressionPropertyNames(node);
|
|
1930
|
+
return (
|
|
1931
|
+
hasPropertyMatching(propertyNames, backendAccessTokenPropertyPattern) &&
|
|
1932
|
+
!hasPropertyMatching(propertyNames, backendRefreshTokenPropertyPattern)
|
|
1933
|
+
);
|
|
1934
|
+
};
|
|
1935
|
+
|
|
1936
|
+
export const findBackendAuthResponseWithoutRefreshTokenLines = (ast: unknown): readonly number[] => {
|
|
1937
|
+
return collectLineMatchesWithAncestors(ast, (node, ancestors) => {
|
|
1938
|
+
if (!isBackendAuthResponseObjectWithoutRefreshToken(node)) {
|
|
1939
|
+
return false;
|
|
1940
|
+
}
|
|
1941
|
+
const parent = ancestors[ancestors.length - 1];
|
|
1942
|
+
if (isObject(parent) && parent.type === 'ReturnStatement' && parent.argument === node) {
|
|
1943
|
+
return true;
|
|
1944
|
+
}
|
|
1945
|
+
return ancestors.some((ancestor) => {
|
|
1946
|
+
if (ancestor.type !== 'CallExpression' || !Array.isArray(ancestor.arguments)) {
|
|
1947
|
+
return false;
|
|
1948
|
+
}
|
|
1949
|
+
if (!ancestor.arguments.includes(node) || !isObject(ancestor.callee)) {
|
|
1950
|
+
return false;
|
|
1951
|
+
}
|
|
1952
|
+
const methodName = memberExpressionPropertyName(ancestor.callee);
|
|
1953
|
+
return methodName !== undefined && httpResponseMethodNames.has(methodName);
|
|
1954
|
+
});
|
|
1955
|
+
});
|
|
1956
|
+
};
|
|
1957
|
+
|
|
1958
|
+
export const hasBackendAuthResponseWithoutRefreshToken = (ast: unknown): boolean =>
|
|
1959
|
+
findBackendAuthResponseWithoutRefreshTokenLines(ast).length > 0;
|
|
1960
|
+
|
|
1961
|
+
const isProcessEnvMemberExpression = (node: unknown): boolean => {
|
|
1962
|
+
if (!isObject(node) || node.type !== 'MemberExpression') {
|
|
1963
|
+
return false;
|
|
1964
|
+
}
|
|
1965
|
+
const propertyName = memberExpressionPropertyName(node);
|
|
1966
|
+
if (typeof propertyName !== 'string' || propertyName.length === 0) {
|
|
1967
|
+
return false;
|
|
1968
|
+
}
|
|
1969
|
+
const objectNode = node.object;
|
|
1970
|
+
return (
|
|
1971
|
+
isObject(objectNode) &&
|
|
1972
|
+
objectNode.type === 'MemberExpression' &&
|
|
1973
|
+
memberExpressionObjectName(objectNode) === 'process' &&
|
|
1974
|
+
memberExpressionPropertyName(objectNode) === 'env'
|
|
1975
|
+
);
|
|
1976
|
+
};
|
|
1977
|
+
|
|
1978
|
+
const isLiteralConfigFallback = (node: unknown): boolean => {
|
|
1979
|
+
if (!isObject(node)) {
|
|
1980
|
+
return false;
|
|
1981
|
+
}
|
|
1982
|
+
return (
|
|
1983
|
+
node.type === 'StringLiteral' ||
|
|
1984
|
+
node.type === 'NumericLiteral' ||
|
|
1985
|
+
node.type === 'BooleanLiteral' ||
|
|
1986
|
+
(node.type === 'TemplateLiteral' &&
|
|
1987
|
+
Array.isArray(node.expressions) &&
|
|
1988
|
+
node.expressions.length === 0)
|
|
1989
|
+
);
|
|
1990
|
+
};
|
|
1991
|
+
|
|
1992
|
+
const isBackendProcessEnvDefaultFallback = (node: AstNode): boolean => {
|
|
1993
|
+
return (
|
|
1994
|
+
node.type === 'LogicalExpression' &&
|
|
1995
|
+
(node.operator === '||' || node.operator === '??') &&
|
|
1996
|
+
isProcessEnvMemberExpression(node.left) &&
|
|
1997
|
+
isLiteralConfigFallback(node.right)
|
|
1998
|
+
);
|
|
1999
|
+
};
|
|
2000
|
+
|
|
2001
|
+
export const findBackendProcessEnvDefaultFallbackLines = (ast: unknown): readonly number[] =>
|
|
2002
|
+
collectLineMatchesWithAncestors(ast, (node) => isBackendProcessEnvDefaultFallback(node), {
|
|
2003
|
+
max: 12,
|
|
2004
|
+
});
|
|
2005
|
+
|
|
2006
|
+
export const hasBackendProcessEnvDefaultFallback = (ast: unknown): boolean =>
|
|
2007
|
+
findBackendProcessEnvDefaultFallbackLines(ast).length > 0;
|
|
2008
|
+
|
|
2009
|
+
export const findBackendDirectProcessEnvReadLines = (ast: unknown): readonly number[] =>
|
|
2010
|
+
collectLineMatchesWithAncestors(ast, (node) => isProcessEnvMemberExpression(node), {
|
|
2011
|
+
max: 12,
|
|
2012
|
+
});
|
|
2013
|
+
|
|
2014
|
+
export const hasBackendDirectProcessEnvRead = (ast: unknown): boolean =>
|
|
2015
|
+
findBackendDirectProcessEnvReadLines(ast).length > 0;
|
|
2016
|
+
|
|
2017
|
+
const isConfigModuleForRootCall = (node: AstNode): boolean => {
|
|
2018
|
+
if (node.type !== 'CallExpression' || !isObject(node.callee)) {
|
|
2019
|
+
return false;
|
|
2020
|
+
}
|
|
2021
|
+
const callee = node.callee;
|
|
2022
|
+
return (
|
|
2023
|
+
callee.type === 'MemberExpression' &&
|
|
2024
|
+
memberExpressionObjectName(callee) === 'ConfigModule' &&
|
|
2025
|
+
memberExpressionPropertyName(callee) === 'forRoot'
|
|
2026
|
+
);
|
|
2027
|
+
};
|
|
2028
|
+
|
|
2029
|
+
const hasConfigValidationOption = (node: unknown): boolean => {
|
|
2030
|
+
if (!isObject(node) || node.type !== 'ObjectExpression') {
|
|
2031
|
+
return false;
|
|
2032
|
+
}
|
|
2033
|
+
const propertyNames = objectExpressionPropertyNames(node);
|
|
2034
|
+
return propertyNames.has('validationSchema') || propertyNames.has('validate');
|
|
2035
|
+
};
|
|
2036
|
+
|
|
2037
|
+
const isBackendConfigModuleForRootWithoutValidation = (node: AstNode): boolean => {
|
|
2038
|
+
if (!isConfigModuleForRootCall(node)) {
|
|
2039
|
+
return false;
|
|
2040
|
+
}
|
|
2041
|
+
const args = Array.isArray(node.arguments) ? node.arguments : [];
|
|
2042
|
+
const options = args.find((argument) => isObject(argument) && argument.type === 'ObjectExpression');
|
|
2043
|
+
return !hasConfigValidationOption(options);
|
|
2044
|
+
};
|
|
2045
|
+
|
|
2046
|
+
export const findBackendConfigModuleWithoutValidationLines = (ast: unknown): readonly number[] =>
|
|
2047
|
+
collectLineMatchesWithAncestors(
|
|
2048
|
+
ast,
|
|
2049
|
+
(node) => isBackendConfigModuleForRootWithoutValidation(node),
|
|
2050
|
+
{ max: 12 }
|
|
2051
|
+
);
|
|
2052
|
+
|
|
2053
|
+
export const hasBackendConfigModuleWithoutValidation = (ast: unknown): boolean =>
|
|
2054
|
+
findBackendConfigModuleWithoutValidationLines(ast).length > 0;
|
|
2055
|
+
|
|
1913
2056
|
const isErrorStatusCallExpression = (node: unknown): boolean => {
|
|
1914
2057
|
if (!isObject(node) || node.type !== 'CallExpression') {
|
|
1915
2058
|
return false;
|
|
@@ -2155,7 +2298,7 @@ const isCorsPermissiveValue = (node: unknown): boolean => {
|
|
|
2155
2298
|
);
|
|
2156
2299
|
};
|
|
2157
2300
|
|
|
2158
|
-
const
|
|
2301
|
+
const validationPipeObjectPropertyName = (node: unknown): string | undefined => {
|
|
2159
2302
|
if (!isObject(node) || (node.type !== 'ObjectProperty' && node.type !== 'Property')) {
|
|
2160
2303
|
return undefined;
|
|
2161
2304
|
}
|
|
@@ -2259,6 +2402,34 @@ export const findBackendStringLiteralUnionEnumCandidateLines = (
|
|
|
2259
2402
|
export const hasBackendStringLiteralUnionEnumCandidate = (ast: unknown): boolean =>
|
|
2260
2403
|
findBackendStringLiteralUnionEnumCandidateLines(ast).length > 0;
|
|
2261
2404
|
|
|
2405
|
+
const backendCacheWriteMethodNames = new Set(['hset', 'mset', 'set', 'setex']);
|
|
2406
|
+
const backendCacheObjectPattern = /(cache|cacheManager|redis)/i;
|
|
2407
|
+
|
|
2408
|
+
const isBackendSensitiveCacheWriteCall = (node: AstNode): boolean => {
|
|
2409
|
+
if (node.type !== 'CallExpression' || !isObject(node.callee)) {
|
|
2410
|
+
return false;
|
|
2411
|
+
}
|
|
2412
|
+
const callee = node.callee;
|
|
2413
|
+
const methodName = memberExpressionPropertyName(callee);
|
|
2414
|
+
if (methodName === undefined || !backendCacheWriteMethodNames.has(methodName)) {
|
|
2415
|
+
return false;
|
|
2416
|
+
}
|
|
2417
|
+
const objectName =
|
|
2418
|
+
callee.type === 'MemberExpression' ? identifierNameFromNode(callee.object) : undefined;
|
|
2419
|
+
if (objectName === undefined || !backendCacheObjectPattern.test(objectName)) {
|
|
2420
|
+
return false;
|
|
2421
|
+
}
|
|
2422
|
+
const args = Array.isArray(node.arguments) ? node.arguments : [];
|
|
2423
|
+
return args.some(hasSensitiveIdentifierReference);
|
|
2424
|
+
};
|
|
2425
|
+
|
|
2426
|
+
export const findBackendSensitiveCacheWriteLines = (ast: unknown): readonly number[] => {
|
|
2427
|
+
return collectLineMatchesWithAncestors(ast, (node) => isBackendSensitiveCacheWriteCall(node));
|
|
2428
|
+
};
|
|
2429
|
+
|
|
2430
|
+
export const hasBackendSensitiveCacheWrite = (ast: unknown): boolean =>
|
|
2431
|
+
findBackendSensitiveCacheWriteLines(ast).length > 0;
|
|
2432
|
+
|
|
2262
2433
|
const isLoggingCall = (node: AstNode): boolean => {
|
|
2263
2434
|
if (node.type !== 'CallExpression' || !isObject(node.callee)) {
|
|
2264
2435
|
return false;
|
|
@@ -2766,6 +2937,12 @@ const nestJsClassDecoratorNames = new Set([
|
|
|
2766
2937
|
]);
|
|
2767
2938
|
const nestJsRouteDecoratorNames = new Set(['Delete', 'Get', 'Head', 'Options', 'Patch', 'Post', 'Put']);
|
|
2768
2939
|
const nestJsUseGuardsDecoratorNames = new Set(['UseGuards']);
|
|
2940
|
+
const nestJsRolesDecoratorNames = new Set(['Roles']);
|
|
2941
|
+
const nestJsOnEventDecoratorNames = new Set(['OnEvent']);
|
|
2942
|
+
const nestJsThrottleDecoratorNames = new Set(['Throttle']);
|
|
2943
|
+
const backendEventHandlerNamePattern = /(^handle[A-Z].*Event$|EventHandler$|EventsHandler$)/;
|
|
2944
|
+
const backendAuthRouteNamePattern =
|
|
2945
|
+
/(forgot|login|password|refresh|register|reset|signin|sign-in|signup|sign-up|token)/i;
|
|
2769
2946
|
|
|
2770
2947
|
const decoratorExpressionName = (node: unknown): string | undefined => {
|
|
2771
2948
|
if (!isObject(node)) {
|
|
@@ -2801,12 +2978,42 @@ const hasDecoratorFrom = (node: AstNode, allowedNames: ReadonlySet<string>): boo
|
|
|
2801
2978
|
});
|
|
2802
2979
|
};
|
|
2803
2980
|
|
|
2981
|
+
const decoratorCallArguments = (decorator: AstNode): readonly unknown[] => {
|
|
2982
|
+
const expression = decorator.expression;
|
|
2983
|
+
if (!isObject(expression) || expression.type !== 'CallExpression') {
|
|
2984
|
+
return [];
|
|
2985
|
+
}
|
|
2986
|
+
return Array.isArray(expression.arguments) ? expression.arguments : [];
|
|
2987
|
+
};
|
|
2988
|
+
|
|
2804
2989
|
const hasNestJsRouteDecorator = (node: AstNode): boolean =>
|
|
2805
2990
|
hasDecoratorFrom(node, nestJsRouteDecoratorNames);
|
|
2806
2991
|
|
|
2807
2992
|
const hasNestJsUseGuardsDecorator = (node: AstNode): boolean =>
|
|
2808
2993
|
hasDecoratorFrom(node, nestJsUseGuardsDecoratorNames);
|
|
2809
2994
|
|
|
2995
|
+
const isRolesSetMetadataDecorator = (decorator: AstNode): boolean => {
|
|
2996
|
+
const name = decoratorExpressionName(decorator.expression);
|
|
2997
|
+
if (name !== 'SetMetadata') {
|
|
2998
|
+
return false;
|
|
2999
|
+
}
|
|
3000
|
+
const [metadataKey] = decoratorCallArguments(decorator);
|
|
3001
|
+
return isObject(metadataKey) && metadataKey.type === 'StringLiteral' && metadataKey.value === 'roles';
|
|
3002
|
+
};
|
|
3003
|
+
|
|
3004
|
+
const hasNestJsRolesDecorator = (node: AstNode): boolean => {
|
|
3005
|
+
if (!Array.isArray(node.decorators)) {
|
|
3006
|
+
return false;
|
|
3007
|
+
}
|
|
3008
|
+
return node.decorators.some((decorator) => {
|
|
3009
|
+
if (!isObject(decorator)) {
|
|
3010
|
+
return false;
|
|
3011
|
+
}
|
|
3012
|
+
const name = decoratorExpressionName(decorator.expression);
|
|
3013
|
+
return (name !== undefined && nestJsRolesDecoratorNames.has(name)) || isRolesSetMetadataDecorator(decorator);
|
|
3014
|
+
});
|
|
3015
|
+
};
|
|
3016
|
+
|
|
2810
3017
|
const isClassLikeNode = (node: unknown): node is AstNode =>
|
|
2811
3018
|
isObject(node) && (node.type === 'ClassDeclaration' || node.type === 'ClassExpression');
|
|
2812
3019
|
|
|
@@ -2926,6 +3133,102 @@ export const findBackendControllerRouteWithoutGuardLines = (ast: unknown): reado
|
|
|
2926
3133
|
export const hasBackendControllerRouteWithoutGuard = (ast: unknown): boolean =>
|
|
2927
3134
|
findBackendControllerRouteWithoutGuardLines(ast).length > 0;
|
|
2928
3135
|
|
|
3136
|
+
export const findBackendControllerRouteWithoutRolesLines = (ast: unknown): readonly number[] => {
|
|
3137
|
+
return collectLineMatchesWithAncestors(ast, (node, ancestors) => {
|
|
3138
|
+
if (!isClassMethodLikeNode(node) || !hasNestJsRouteDecorator(node)) {
|
|
3139
|
+
return false;
|
|
3140
|
+
}
|
|
3141
|
+
const ownerClass = [...ancestors].reverse().find(isClassLikeNode);
|
|
3142
|
+
const hasGuard = hasNestJsUseGuardsDecorator(node) || (ownerClass !== undefined && hasNestJsUseGuardsDecorator(ownerClass));
|
|
3143
|
+
if (!hasGuard) {
|
|
3144
|
+
return false;
|
|
3145
|
+
}
|
|
3146
|
+
return !hasNestJsRolesDecorator(node) && (ownerClass === undefined || !hasNestJsRolesDecorator(ownerClass));
|
|
3147
|
+
});
|
|
3148
|
+
};
|
|
3149
|
+
|
|
3150
|
+
export const hasBackendControllerRouteWithoutRoles = (ast: unknown): boolean =>
|
|
3151
|
+
findBackendControllerRouteWithoutRolesLines(ast).length > 0;
|
|
3152
|
+
|
|
3153
|
+
const isBackendEventHandlerClassOrMethod = (node: AstNode): boolean => {
|
|
3154
|
+
if (node.type === 'ClassDeclaration' || node.type === 'ClassExpression') {
|
|
3155
|
+
return backendEventHandlerNamePattern.test(classNameFromNode(node));
|
|
3156
|
+
}
|
|
3157
|
+
if (!isClassMethodLikeNode(node)) {
|
|
3158
|
+
return false;
|
|
3159
|
+
}
|
|
3160
|
+
const methodName = methodNameFromNode(node.key);
|
|
3161
|
+
return typeof methodName === 'string' && backendEventHandlerNamePattern.test(methodName);
|
|
3162
|
+
};
|
|
3163
|
+
|
|
3164
|
+
const hasOnEventDecoratedClassMember = (node: AstNode): boolean => {
|
|
3165
|
+
if (node.type !== 'ClassDeclaration' && node.type !== 'ClassExpression') {
|
|
3166
|
+
return false;
|
|
3167
|
+
}
|
|
3168
|
+
const members = isObject(node.body) && Array.isArray(node.body.body) ? node.body.body : [];
|
|
3169
|
+
return members.some((member) => isObject(member) && hasDecoratorFrom(member, nestJsOnEventDecoratorNames));
|
|
3170
|
+
};
|
|
3171
|
+
|
|
3172
|
+
export const findBackendEventHandlerWithoutOnEventLines = (ast: unknown): readonly number[] => {
|
|
3173
|
+
return collectLineMatchesWithAncestors(ast, (node, ancestors) => {
|
|
3174
|
+
if (!isBackendEventHandlerClassOrMethod(node)) {
|
|
3175
|
+
return false;
|
|
3176
|
+
}
|
|
3177
|
+
if (hasDecoratorFrom(node, nestJsOnEventDecoratorNames)) {
|
|
3178
|
+
return false;
|
|
3179
|
+
}
|
|
3180
|
+
if (isClassLikeNode(node) && hasOnEventDecoratedClassMember(node)) {
|
|
3181
|
+
return false;
|
|
3182
|
+
}
|
|
3183
|
+
const ownerClass = [...ancestors].reverse().find(isClassLikeNode);
|
|
3184
|
+
return ownerClass === undefined || !hasDecoratorFrom(ownerClass, nestJsOnEventDecoratorNames);
|
|
3185
|
+
});
|
|
3186
|
+
};
|
|
3187
|
+
|
|
3188
|
+
export const hasBackendEventHandlerWithoutOnEvent = (ast: unknown): boolean =>
|
|
3189
|
+
findBackendEventHandlerWithoutOnEventLines(ast).length > 0;
|
|
3190
|
+
|
|
3191
|
+
const isBackendAuthRouteMethod = (node: AstNode): boolean => {
|
|
3192
|
+
if (!isClassMethodLikeNode(node) || !hasNestJsRouteDecorator(node)) {
|
|
3193
|
+
return false;
|
|
3194
|
+
}
|
|
3195
|
+
const methodName = methodNameFromNode(node.key);
|
|
3196
|
+
if (methodName !== undefined && backendAuthRouteNamePattern.test(methodName)) {
|
|
3197
|
+
return true;
|
|
3198
|
+
}
|
|
3199
|
+
if (!Array.isArray(node.decorators)) {
|
|
3200
|
+
return false;
|
|
3201
|
+
}
|
|
3202
|
+
return node.decorators.some((decorator) => {
|
|
3203
|
+
if (!isObject(decorator)) {
|
|
3204
|
+
return false;
|
|
3205
|
+
}
|
|
3206
|
+
const decoratorName = decoratorExpressionName(decorator.expression);
|
|
3207
|
+
if (decoratorName === undefined || !nestJsRouteDecoratorNames.has(decoratorName)) {
|
|
3208
|
+
return false;
|
|
3209
|
+
}
|
|
3210
|
+
const [firstArgument] = decoratorCallArguments(decorator);
|
|
3211
|
+
const routePath = literalTextFromNode(firstArgument);
|
|
3212
|
+
return routePath !== undefined && backendAuthRouteNamePattern.test(routePath);
|
|
3213
|
+
});
|
|
3214
|
+
};
|
|
3215
|
+
|
|
3216
|
+
export const findBackendAuthRouteWithoutThrottleLines = (ast: unknown): readonly number[] => {
|
|
3217
|
+
return collectLineMatchesWithAncestors(ast, (node, ancestors) => {
|
|
3218
|
+
if (!isBackendAuthRouteMethod(node)) {
|
|
3219
|
+
return false;
|
|
3220
|
+
}
|
|
3221
|
+
if (hasDecoratorFrom(node, nestJsThrottleDecoratorNames)) {
|
|
3222
|
+
return false;
|
|
3223
|
+
}
|
|
3224
|
+
const ownerClass = [...ancestors].reverse().find(isClassLikeNode);
|
|
3225
|
+
return ownerClass === undefined || !hasDecoratorFrom(ownerClass, nestJsThrottleDecoratorNames);
|
|
3226
|
+
});
|
|
3227
|
+
};
|
|
3228
|
+
|
|
3229
|
+
export const hasBackendAuthRouteWithoutThrottle = (ast: unknown): boolean =>
|
|
3230
|
+
findBackendAuthRouteWithoutThrottleLines(ast).length > 0;
|
|
3231
|
+
|
|
2929
3232
|
const persistenceMutationNames = new Set([
|
|
2930
3233
|
'create',
|
|
2931
3234
|
'delete',
|
|
@@ -3128,6 +3431,19 @@ const hasClassValidatorDecorator = (node: AstNode): boolean => {
|
|
|
3128
3431
|
});
|
|
3129
3432
|
};
|
|
3130
3433
|
|
|
3434
|
+
const hasApiPropertyDecorator = (node: AstNode): boolean => {
|
|
3435
|
+
if (!Array.isArray(node.decorators)) {
|
|
3436
|
+
return false;
|
|
3437
|
+
}
|
|
3438
|
+
return node.decorators.some((decorator) => {
|
|
3439
|
+
if (!isObject(decorator)) {
|
|
3440
|
+
return false;
|
|
3441
|
+
}
|
|
3442
|
+
const name = decoratorExpressionName(decorator.expression);
|
|
3443
|
+
return name === 'ApiProperty' || name === 'ApiPropertyOptional';
|
|
3444
|
+
});
|
|
3445
|
+
};
|
|
3446
|
+
|
|
3131
3447
|
const hasDtoNestedValidationDecorator = (node: AstNode): boolean => {
|
|
3132
3448
|
if (!Array.isArray(node.decorators)) {
|
|
3133
3449
|
return false;
|
|
@@ -3227,6 +3543,19 @@ export const findDtoPropertyWithoutValidationMatch = (
|
|
|
3227
3543
|
export const hasDtoPropertyWithoutValidation = (ast: unknown): boolean =>
|
|
3228
3544
|
findDtoPropertyWithoutValidationMatch(ast) !== undefined;
|
|
3229
3545
|
|
|
3546
|
+
export const findDtoPropertyWithoutApiPropertyLines = (ast: unknown): readonly number[] =>
|
|
3547
|
+
collectLineMatchesWithAncestors(ast, (node, ancestors) => {
|
|
3548
|
+
if (!isDtoPropertyNode(node) || hasApiPropertyDecorator(node)) {
|
|
3549
|
+
return false;
|
|
3550
|
+
}
|
|
3551
|
+
return ancestors.some(
|
|
3552
|
+
(ancestor) => ancestor.type === 'ClassDeclaration' && isDtoClassName(classNameFromNode(ancestor))
|
|
3553
|
+
);
|
|
3554
|
+
}, { max: 12 });
|
|
3555
|
+
|
|
3556
|
+
export const hasDtoPropertyWithoutApiProperty = (ast: unknown): boolean =>
|
|
3557
|
+
findDtoPropertyWithoutApiPropertyLines(ast).length > 0;
|
|
3558
|
+
|
|
3230
3559
|
export type TypeScriptDtoNestedPropertyWithoutNestedValidationMatch = {
|
|
3231
3560
|
lines: readonly number[];
|
|
3232
3561
|
primary_node: string;
|
|
@@ -3290,6 +3619,329 @@ export const findDtoNestedPropertyWithoutNestedValidationMatch = (
|
|
|
3290
3619
|
export const hasDtoNestedPropertyWithoutNestedValidation = (ast: unknown): boolean =>
|
|
3291
3620
|
findDtoNestedPropertyWithoutNestedValidationMatch(ast) !== undefined;
|
|
3292
3621
|
|
|
3622
|
+
const isValidationPipeCallee = (node: unknown): boolean => {
|
|
3623
|
+
return isObject(node) && node.type === 'Identifier' && node.name === 'ValidationPipe';
|
|
3624
|
+
};
|
|
3625
|
+
|
|
3626
|
+
const objectPropertyName = (node: unknown): string | undefined => {
|
|
3627
|
+
if (!isObject(node)) {
|
|
3628
|
+
return undefined;
|
|
3629
|
+
}
|
|
3630
|
+
if (isObject(node.key)) {
|
|
3631
|
+
return methodNameFromNode(node.key);
|
|
3632
|
+
}
|
|
3633
|
+
return undefined;
|
|
3634
|
+
};
|
|
3635
|
+
|
|
3636
|
+
const isBooleanTrueLiteral = (node: unknown): boolean => {
|
|
3637
|
+
return isObject(node) && node.type === 'BooleanLiteral' && node.value === true;
|
|
3638
|
+
};
|
|
3639
|
+
|
|
3640
|
+
const validationPipeOptionsEnableWhitelist = (node: unknown): boolean => {
|
|
3641
|
+
if (!isObject(node) || node.type !== 'ObjectExpression' || !Array.isArray(node.properties)) {
|
|
3642
|
+
return false;
|
|
3643
|
+
}
|
|
3644
|
+
return node.properties.some((property) => {
|
|
3645
|
+
return (
|
|
3646
|
+
isObject(property) &&
|
|
3647
|
+
validationPipeObjectPropertyName(property) === 'whitelist' &&
|
|
3648
|
+
isBooleanTrueLiteral(property.value)
|
|
3649
|
+
);
|
|
3650
|
+
});
|
|
3651
|
+
};
|
|
3652
|
+
|
|
3653
|
+
const isStrictValidationPipeExpression = (node: unknown): boolean => {
|
|
3654
|
+
if (!isObject(node)) {
|
|
3655
|
+
return false;
|
|
3656
|
+
}
|
|
3657
|
+
if (node.type !== 'NewExpression' && node.type !== 'CallExpression') {
|
|
3658
|
+
return false;
|
|
3659
|
+
}
|
|
3660
|
+
if (!isValidationPipeCallee(node.callee)) {
|
|
3661
|
+
return false;
|
|
3662
|
+
}
|
|
3663
|
+
const args = Array.isArray(node.arguments) ? node.arguments : [];
|
|
3664
|
+
return args.some(validationPipeOptionsEnableWhitelist);
|
|
3665
|
+
};
|
|
3666
|
+
|
|
3667
|
+
const isUseGlobalPipesCall = (node: AstNode): boolean => {
|
|
3668
|
+
return node.type === 'CallExpression' && callExpressionMemberName(node) === 'useGlobalPipes';
|
|
3669
|
+
};
|
|
3670
|
+
|
|
3671
|
+
const isStrictUseGlobalPipesCall = (node: AstNode): boolean => {
|
|
3672
|
+
if (!isUseGlobalPipesCall(node)) {
|
|
3673
|
+
return false;
|
|
3674
|
+
}
|
|
3675
|
+
const args = Array.isArray(node.arguments) ? node.arguments : [];
|
|
3676
|
+
return args.some(isStrictValidationPipeExpression);
|
|
3677
|
+
};
|
|
3678
|
+
|
|
3679
|
+
const isBackendValidationNestFactoryCreateCall = (node: AstNode): boolean => {
|
|
3680
|
+
if (node.type !== 'CallExpression') {
|
|
3681
|
+
return false;
|
|
3682
|
+
}
|
|
3683
|
+
const memberName = callExpressionMemberName(node);
|
|
3684
|
+
const objectName = callExpressionObjectName(node);
|
|
3685
|
+
return memberName === 'create' && objectName === 'NestFactory';
|
|
3686
|
+
};
|
|
3687
|
+
|
|
3688
|
+
export const findBackendMissingGlobalValidationPipeLines = (ast: unknown): readonly number[] => {
|
|
3689
|
+
const unsafeUseGlobalPipesLines = collectLineMatchesWithAncestors(
|
|
3690
|
+
ast,
|
|
3691
|
+
(node) => isUseGlobalPipesCall(node) && !isStrictUseGlobalPipesCall(node)
|
|
3692
|
+
);
|
|
3693
|
+
if (unsafeUseGlobalPipesLines.length > 0) {
|
|
3694
|
+
return unsafeUseGlobalPipesLines;
|
|
3695
|
+
}
|
|
3696
|
+
|
|
3697
|
+
if (!hasNode(ast, isBackendValidationNestFactoryCreateCall)) {
|
|
3698
|
+
return [];
|
|
3699
|
+
}
|
|
3700
|
+
if (hasNode(ast, isStrictUseGlobalPipesCall)) {
|
|
3701
|
+
return [];
|
|
3702
|
+
}
|
|
3703
|
+
return collectLineMatchesWithAncestors(ast, isBackendValidationNestFactoryCreateCall, { max: 1 });
|
|
3704
|
+
};
|
|
3705
|
+
|
|
3706
|
+
export const hasBackendMissingGlobalValidationPipe = (ast: unknown): boolean =>
|
|
3707
|
+
findBackendMissingGlobalValidationPipeLines(ast).length > 0;
|
|
3708
|
+
|
|
3709
|
+
const isHelmetCallee = (node: unknown): boolean => {
|
|
3710
|
+
return isObject(node) && node.type === 'Identifier' && node.name === 'helmet';
|
|
3711
|
+
};
|
|
3712
|
+
|
|
3713
|
+
const isHelmetCallExpression = (node: unknown): boolean => {
|
|
3714
|
+
return isObject(node) && node.type === 'CallExpression' && isHelmetCallee(node.callee);
|
|
3715
|
+
};
|
|
3716
|
+
|
|
3717
|
+
const isAppUseHelmetCall = (node: AstNode): boolean => {
|
|
3718
|
+
if (node.type !== 'CallExpression' || callExpressionMemberName(node) !== 'use') {
|
|
3719
|
+
return false;
|
|
3720
|
+
}
|
|
3721
|
+
const args = Array.isArray(node.arguments) ? node.arguments : [];
|
|
3722
|
+
return args.some(isHelmetCallExpression);
|
|
3723
|
+
};
|
|
3724
|
+
|
|
3725
|
+
export const findBackendMissingHelmetSecurityHeadersLines = (ast: unknown): readonly number[] => {
|
|
3726
|
+
if (!hasNode(ast, isBackendValidationNestFactoryCreateCall)) {
|
|
3727
|
+
return [];
|
|
3728
|
+
}
|
|
3729
|
+
if (hasNode(ast, isAppUseHelmetCall)) {
|
|
3730
|
+
return [];
|
|
3731
|
+
}
|
|
3732
|
+
return collectLineMatchesWithAncestors(ast, isBackendValidationNestFactoryCreateCall, { max: 1 });
|
|
3733
|
+
};
|
|
3734
|
+
|
|
3735
|
+
export const hasBackendMissingHelmetSecurityHeaders = (ast: unknown): boolean =>
|
|
3736
|
+
findBackendMissingHelmetSecurityHeadersLines(ast).length > 0;
|
|
3737
|
+
|
|
3738
|
+
const isCompressionCallee = (node: unknown): boolean => {
|
|
3739
|
+
return isObject(node) && node.type === 'Identifier' && node.name === 'compression';
|
|
3740
|
+
};
|
|
3741
|
+
|
|
3742
|
+
const isCompressionCallExpression = (node: unknown): boolean => {
|
|
3743
|
+
return isObject(node) && node.type === 'CallExpression' && isCompressionCallee(node.callee);
|
|
3744
|
+
};
|
|
3745
|
+
|
|
3746
|
+
const isAppUseCompressionCall = (node: AstNode): boolean => {
|
|
3747
|
+
if (node.type !== 'CallExpression' || callExpressionMemberName(node) !== 'use') {
|
|
3748
|
+
return false;
|
|
3749
|
+
}
|
|
3750
|
+
const args = Array.isArray(node.arguments) ? node.arguments : [];
|
|
3751
|
+
return args.some(isCompressionCallExpression);
|
|
3752
|
+
};
|
|
3753
|
+
|
|
3754
|
+
export const findBackendMissingCompressionMiddlewareLines = (
|
|
3755
|
+
ast: unknown
|
|
3756
|
+
): readonly number[] => {
|
|
3757
|
+
if (!hasNode(ast, isBackendValidationNestFactoryCreateCall)) {
|
|
3758
|
+
return [];
|
|
3759
|
+
}
|
|
3760
|
+
if (hasNode(ast, isAppUseCompressionCall)) {
|
|
3761
|
+
return [];
|
|
3762
|
+
}
|
|
3763
|
+
return collectLineMatchesWithAncestors(ast, isBackendValidationNestFactoryCreateCall, { max: 1 });
|
|
3764
|
+
};
|
|
3765
|
+
|
|
3766
|
+
export const hasBackendMissingCompressionMiddleware = (ast: unknown): boolean =>
|
|
3767
|
+
findBackendMissingCompressionMiddlewareLines(ast).length > 0;
|
|
3768
|
+
|
|
3769
|
+
const backendRouteDecoratorNames = new Set([
|
|
3770
|
+
'All',
|
|
3771
|
+
'Delete',
|
|
3772
|
+
'Get',
|
|
3773
|
+
'Head',
|
|
3774
|
+
'Options',
|
|
3775
|
+
'Patch',
|
|
3776
|
+
'Post',
|
|
3777
|
+
'Put',
|
|
3778
|
+
]);
|
|
3779
|
+
|
|
3780
|
+
const backendControllerBusinessLogicNodeTypes = new Set([
|
|
3781
|
+
'DoWhileStatement',
|
|
3782
|
+
'ForInStatement',
|
|
3783
|
+
'ForOfStatement',
|
|
3784
|
+
'ForStatement',
|
|
3785
|
+
'IfStatement',
|
|
3786
|
+
'SwitchStatement',
|
|
3787
|
+
'WhileStatement',
|
|
3788
|
+
]);
|
|
3789
|
+
|
|
3790
|
+
const hasBackendRouteDecorator = (node: AstNode): boolean => {
|
|
3791
|
+
if (!Array.isArray(node.decorators)) {
|
|
3792
|
+
return false;
|
|
3793
|
+
}
|
|
3794
|
+
return node.decorators.some((decorator) => {
|
|
3795
|
+
if (!isObject(decorator)) {
|
|
3796
|
+
return false;
|
|
3797
|
+
}
|
|
3798
|
+
const name = decoratorExpressionName(decorator.expression);
|
|
3799
|
+
return name !== undefined && backendRouteDecoratorNames.has(name);
|
|
3800
|
+
});
|
|
3801
|
+
};
|
|
3802
|
+
|
|
3803
|
+
const isBackendControllerRouteMethod = (node: AstNode): boolean => {
|
|
3804
|
+
return (
|
|
3805
|
+
(node.type === 'ClassMethod' ||
|
|
3806
|
+
node.type === 'ClassPrivateMethod' ||
|
|
3807
|
+
node.type === 'ObjectMethod') &&
|
|
3808
|
+
hasBackendRouteDecorator(node)
|
|
3809
|
+
);
|
|
3810
|
+
};
|
|
3811
|
+
|
|
3812
|
+
const isBackendControllerBusinessLogicNode = (node: AstNode): boolean => {
|
|
3813
|
+
if (typeof node.type === 'string' && backendControllerBusinessLogicNodeTypes.has(node.type)) {
|
|
3814
|
+
return true;
|
|
3815
|
+
}
|
|
3816
|
+
if (isPersistenceMutationCall(node)) {
|
|
3817
|
+
return true;
|
|
3818
|
+
}
|
|
3819
|
+
const objectName = callExpressionObjectName(node);
|
|
3820
|
+
const rootObjectName =
|
|
3821
|
+
node.type === 'CallExpression' && isObject(node.callee)
|
|
3822
|
+
? memberExpressionRootObjectName(node.callee)
|
|
3823
|
+
: undefined;
|
|
3824
|
+
return (
|
|
3825
|
+
(objectName !== undefined && /(?:repository|repo|prisma|supabase|model|collection)$/i.test(objectName)) ||
|
|
3826
|
+
(rootObjectName !== undefined && /(?:repository|repo|prisma|supabase|model|collection)$/i.test(rootObjectName))
|
|
3827
|
+
);
|
|
3828
|
+
};
|
|
3829
|
+
|
|
3830
|
+
export type TypeScriptBackendControllerBusinessLogicMatch = {
|
|
3831
|
+
lines: readonly number[];
|
|
3832
|
+
primary_node: string;
|
|
3833
|
+
related_nodes: readonly string[];
|
|
3834
|
+
why: string;
|
|
3835
|
+
impact: string;
|
|
3836
|
+
expected_fix: string;
|
|
3837
|
+
};
|
|
3838
|
+
|
|
3839
|
+
export const findBackendControllerBusinessLogicMatch = (
|
|
3840
|
+
ast: unknown
|
|
3841
|
+
): TypeScriptBackendControllerBusinessLogicMatch | undefined => {
|
|
3842
|
+
const match = findFirstNodeWithAncestors(ast, (node) => {
|
|
3843
|
+
if (!isBackendControllerRouteMethod(node)) {
|
|
3844
|
+
return false;
|
|
3845
|
+
}
|
|
3846
|
+
return hasDescendantNode(
|
|
3847
|
+
node,
|
|
3848
|
+
(nested) => nested !== node && isBackendControllerBusinessLogicNode(nested)
|
|
3849
|
+
);
|
|
3850
|
+
});
|
|
3851
|
+
|
|
3852
|
+
if (match === undefined) {
|
|
3853
|
+
return undefined;
|
|
3854
|
+
}
|
|
3855
|
+
|
|
3856
|
+
const methodName = methodNameFromNode(match.node.key) ?? 'anonymous controller route';
|
|
3857
|
+
const line = toPositiveLine(match.node);
|
|
3858
|
+
|
|
3859
|
+
return {
|
|
3860
|
+
lines: line === null ? [] : sortedUniqueLines([line]),
|
|
3861
|
+
primary_node: methodName,
|
|
3862
|
+
related_nodes: ['NestJS route handler', 'business logic in controller'],
|
|
3863
|
+
why: `El handler ${methodName} contiene logica de negocio o acceso a persistencia dentro del controller.`,
|
|
3864
|
+
impact:
|
|
3865
|
+
'El controller deja de ser una frontera HTTP delgada, mezcla routing con dominio/aplicacion y dificulta testabilidad y reutilizacion.',
|
|
3866
|
+
expected_fix:
|
|
3867
|
+
'Mueve la logica a un service/use case inyectado y deja el controller solo con routing, validacion de entrada y delegacion.',
|
|
3868
|
+
};
|
|
3869
|
+
};
|
|
3870
|
+
|
|
3871
|
+
export const findBackendControllerBusinessLogicLines = (ast: unknown): readonly number[] =>
|
|
3872
|
+
findBackendControllerBusinessLogicMatch(ast)?.lines ?? [];
|
|
3873
|
+
|
|
3874
|
+
export const hasBackendControllerBusinessLogic = (ast: unknown): boolean =>
|
|
3875
|
+
findBackendControllerBusinessLogicMatch(ast) !== undefined;
|
|
3876
|
+
|
|
3877
|
+
const isBackendRepositoryClassName = (name: string | undefined): boolean =>
|
|
3878
|
+
name !== undefined && /(?:Repository|Repo)$/i.test(name);
|
|
3879
|
+
|
|
3880
|
+
const isBackendRepositoryBusinessLogicMethod = (node: AstNode): boolean => {
|
|
3881
|
+
if (
|
|
3882
|
+
node.type !== 'ClassMethod' &&
|
|
3883
|
+
node.type !== 'ClassPrivateMethod' &&
|
|
3884
|
+
node.type !== 'ObjectMethod'
|
|
3885
|
+
) {
|
|
3886
|
+
return false;
|
|
3887
|
+
}
|
|
3888
|
+
return hasDescendantNode(node, (nested) => {
|
|
3889
|
+
return (
|
|
3890
|
+
nested !== node &&
|
|
3891
|
+
typeof nested.type === 'string' &&
|
|
3892
|
+
backendControllerBusinessLogicNodeTypes.has(nested.type)
|
|
3893
|
+
);
|
|
3894
|
+
});
|
|
3895
|
+
};
|
|
3896
|
+
|
|
3897
|
+
export type TypeScriptBackendRepositoryBusinessLogicMatch = {
|
|
3898
|
+
lines: readonly number[];
|
|
3899
|
+
primary_node: string;
|
|
3900
|
+
related_nodes: readonly string[];
|
|
3901
|
+
why: string;
|
|
3902
|
+
impact: string;
|
|
3903
|
+
expected_fix: string;
|
|
3904
|
+
};
|
|
3905
|
+
|
|
3906
|
+
export const findBackendRepositoryBusinessLogicMatch = (
|
|
3907
|
+
ast: unknown
|
|
3908
|
+
): TypeScriptBackendRepositoryBusinessLogicMatch | undefined => {
|
|
3909
|
+
const match = findFirstNodeWithAncestors(ast, (node) => {
|
|
3910
|
+
if (node.type !== 'ClassDeclaration' || !isBackendRepositoryClassName(classNameFromNode(node))) {
|
|
3911
|
+
return false;
|
|
3912
|
+
}
|
|
3913
|
+
const body = isObject(node.body) && Array.isArray(node.body.body) ? node.body.body : [];
|
|
3914
|
+
return body.some((member) => isObject(member) && isBackendRepositoryBusinessLogicMethod(member));
|
|
3915
|
+
});
|
|
3916
|
+
|
|
3917
|
+
if (match === undefined) {
|
|
3918
|
+
return undefined;
|
|
3919
|
+
}
|
|
3920
|
+
|
|
3921
|
+
const className = classNameFromNode(match.node) ?? 'Repository';
|
|
3922
|
+
const body = isObject(match.node.body) && Array.isArray(match.node.body.body) ? match.node.body.body : [];
|
|
3923
|
+
const method = body.find((member) => isObject(member) && isBackendRepositoryBusinessLogicMethod(member));
|
|
3924
|
+
const methodName = isObject(method) ? methodNameFromNode(method.key) : undefined;
|
|
3925
|
+
const line = isObject(method) ? toPositiveLine(method) : toPositiveLine(match.node);
|
|
3926
|
+
|
|
3927
|
+
return {
|
|
3928
|
+
lines: line === null ? [] : sortedUniqueLines([line]),
|
|
3929
|
+
primary_node: className,
|
|
3930
|
+
related_nodes: methodName === undefined ? ['repository method'] : [methodName],
|
|
3931
|
+
why: `El repositorio ${className} contiene ramas de decision dentro de un metodo de persistencia.`,
|
|
3932
|
+
impact:
|
|
3933
|
+
'El repositorio mezcla reglas de negocio con acceso a datos, rompiendo Clean Architecture y dificultando pruebas de casos de uso.',
|
|
3934
|
+
expected_fix:
|
|
3935
|
+
'Mueve la decision de negocio a un use case/service y deja el repositorio limitado a CRUD, queries expresivas y mapeo de datos.',
|
|
3936
|
+
};
|
|
3937
|
+
};
|
|
3938
|
+
|
|
3939
|
+
export const findBackendRepositoryBusinessLogicLines = (ast: unknown): readonly number[] =>
|
|
3940
|
+
findBackendRepositoryBusinessLogicMatch(ast)?.lines ?? [];
|
|
3941
|
+
|
|
3942
|
+
export const hasBackendRepositoryBusinessLogic = (ast: unknown): boolean =>
|
|
3943
|
+
findBackendRepositoryBusinessLogicMatch(ast) !== undefined;
|
|
3944
|
+
|
|
3293
3945
|
const isRecordTypeName = (node: unknown): boolean => {
|
|
3294
3946
|
return (
|
|
3295
3947
|
isObject(node) &&
|