pumuki 6.3.293 → 6.3.294
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/VERSION +1 -1
- package/core/facts/detectors/text/ios.test.ts +6 -0
- package/core/facts/detectors/text/ios.ts +41 -6
- package/core/facts/detectors/typescript/index.test.ts +452 -0
- package/core/facts/detectors/typescript/index.ts +341 -1
- package/core/facts/extractHeuristicFacts.ts +7 -2
- package/core/rules/presets/heuristics/typescript.test.ts +19 -1
- package/core/rules/presets/heuristics/typescript.ts +99 -0
- package/integrations/config/skillsDetectorRegistry.ts +32 -0
- package/package.json +1 -1
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
6.3.
|
|
1
|
+
6.3.294
|
|
@@ -98,9 +98,11 @@ import {
|
|
|
98
98
|
hasSwiftNonIBOutletImplicitlyUnwrappedOptionalUsage,
|
|
99
99
|
hasSwiftObservableObjectUsage,
|
|
100
100
|
hasSwiftOnChangeTaskUsage,
|
|
101
|
+
collectSwiftOnChangeTaskLines,
|
|
101
102
|
collectSwiftOnChangeReadonlyVarLines,
|
|
102
103
|
hasSwiftOnChangeReadonlyVarUsage,
|
|
103
104
|
hasSwiftOnAppearTaskUsage,
|
|
105
|
+
collectSwiftOnAppearTaskLines,
|
|
104
106
|
hasSwiftOnTapGestureUsage,
|
|
105
107
|
hasSwiftOperationQueueUsage,
|
|
106
108
|
hasSwiftContainsUserFilterUsage,
|
|
@@ -766,7 +768,9 @@ struct FeedView: View {
|
|
|
766
768
|
`;
|
|
767
769
|
|
|
768
770
|
assert.equal(hasSwiftOnAppearTaskUsage(source), true);
|
|
771
|
+
assert.deepEqual(collectSwiftOnAppearTaskLines(source), [8]);
|
|
769
772
|
assert.equal(hasSwiftOnAppearTaskUsage(safe), false);
|
|
773
|
+
assert.deepEqual(collectSwiftOnAppearTaskLines(safe), []);
|
|
770
774
|
});
|
|
771
775
|
|
|
772
776
|
test('hasSwiftOnChangeTaskUsage detecta Task dentro de onChange y preserva task id', () => {
|
|
@@ -803,7 +807,9 @@ struct SearchView: View {
|
|
|
803
807
|
`;
|
|
804
808
|
|
|
805
809
|
assert.equal(hasSwiftOnChangeTaskUsage(source), true);
|
|
810
|
+
assert.deepEqual(collectSwiftOnChangeTaskLines(source), [8]);
|
|
806
811
|
assert.equal(hasSwiftOnChangeTaskUsage(safe), false);
|
|
812
|
+
assert.deepEqual(collectSwiftOnChangeTaskLines(safe), []);
|
|
807
813
|
});
|
|
808
814
|
|
|
809
815
|
test('hasSwiftOnChangeReadonlyVarUsage detecta var local dentro de onChange y preserva let', () => {
|
|
@@ -699,17 +699,52 @@ export const hasSwiftEmptyCatchUsage = (source: string): boolean => {
|
|
|
699
699
|
};
|
|
700
700
|
|
|
701
701
|
export const hasSwiftOnAppearTaskUsage = (source: string): boolean => {
|
|
702
|
-
|
|
703
|
-
return /\.onAppear\s*\{[\s\S]{0,500}?\bTask\s*(?:\([^)]*\))?\s*\{/.test(sanitized);
|
|
702
|
+
return collectSwiftOnAppearTaskLines(source).length > 0;
|
|
704
703
|
};
|
|
705
704
|
|
|
706
705
|
export const hasSwiftOnChangeTaskUsage = (source: string): boolean => {
|
|
707
|
-
|
|
708
|
-
return /\.onChange\s*\([^)]*\)\s*\{[\s\S]{0,500}?\bTask\s*(?:\([^)]*\))?\s*\{/.test(
|
|
709
|
-
sanitized
|
|
710
|
-
);
|
|
706
|
+
return collectSwiftOnChangeTaskLines(source).length > 0;
|
|
711
707
|
};
|
|
712
708
|
|
|
709
|
+
const collectSwiftLifecycleTaskLines = (
|
|
710
|
+
source: string,
|
|
711
|
+
lifecyclePattern: RegExp
|
|
712
|
+
): readonly number[] => {
|
|
713
|
+
const lines: number[] = [];
|
|
714
|
+
let insideLifecycle = false;
|
|
715
|
+
let braceDepth = 0;
|
|
716
|
+
|
|
717
|
+
source.split(/\r?\n/).forEach((rawLine, index) => {
|
|
718
|
+
const line = stripSwiftLineForSemanticScan(rawLine);
|
|
719
|
+
|
|
720
|
+
if (!insideLifecycle && lifecyclePattern.test(line)) {
|
|
721
|
+
insideLifecycle = true;
|
|
722
|
+
braceDepth = 0;
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
if (insideLifecycle && /\bTask\s*(?:\([^)]*\))?\s*\{/.test(line)) {
|
|
726
|
+
lines.push(index + 1);
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
if (insideLifecycle) {
|
|
730
|
+
const opened = (line.match(/\{/g) ?? []).length;
|
|
731
|
+
const closed = (line.match(/\}/g) ?? []).length;
|
|
732
|
+
braceDepth += opened - closed;
|
|
733
|
+
if (braceDepth <= 0) {
|
|
734
|
+
insideLifecycle = false;
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
return sortedUniqueLines(lines);
|
|
740
|
+
};
|
|
741
|
+
|
|
742
|
+
export const collectSwiftOnAppearTaskLines = (source: string): readonly number[] =>
|
|
743
|
+
collectSwiftLifecycleTaskLines(source, /\.onAppear\s*\{/);
|
|
744
|
+
|
|
745
|
+
export const collectSwiftOnChangeTaskLines = (source: string): readonly number[] =>
|
|
746
|
+
collectSwiftLifecycleTaskLines(source, /\.onChange\s*\([^)]*\)\s*\{/);
|
|
747
|
+
|
|
713
748
|
export const collectSwiftOnChangeReadonlyVarLines = (source: string): readonly number[] => {
|
|
714
749
|
const lines: number[] = [];
|
|
715
750
|
let insideOnChange = false;
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
findDtoPropertyWithoutValidationMatch,
|
|
7
7
|
findDtoNestedPropertyWithoutNestedValidationMatch,
|
|
8
8
|
findBackendControllerRouteWithoutGuardLines,
|
|
9
|
+
findBackendControllerRouteWithoutRolesLines,
|
|
9
10
|
findBackendPermissiveCorsConfigurationLines,
|
|
10
11
|
findBackendStringLiteralUnionEnumCandidateLines,
|
|
11
12
|
findBackendHardDeleteWithoutSoftDeleteLines,
|
|
@@ -13,7 +14,11 @@ import {
|
|
|
13
14
|
findBackendInconsistentErrorResponseLines,
|
|
14
15
|
findBackendErrorPayloadWithSuccessStatusLines,
|
|
15
16
|
findBackendControllerEntityResponseLines,
|
|
17
|
+
findBackendControllerBusinessLogicMatch,
|
|
18
|
+
findBackendRepositoryBusinessLogicMatch,
|
|
16
19
|
findBackendRawThrowExpressionLines,
|
|
20
|
+
findBackendMissingGlobalValidationPipeLines,
|
|
21
|
+
findBackendMissingHelmetSecurityHeadersLines,
|
|
17
22
|
findFrameworkDependencyImportMatch,
|
|
18
23
|
findMixedCommandQueryClassMatch,
|
|
19
24
|
findMixedCommandQueryInterfaceMatch,
|
|
@@ -37,13 +42,18 @@ import {
|
|
|
37
42
|
hasDtoPropertyWithoutValidation,
|
|
38
43
|
hasDtoNestedPropertyWithoutNestedValidation,
|
|
39
44
|
hasBackendControllerRouteWithoutGuard,
|
|
45
|
+
hasBackendControllerRouteWithoutRoles,
|
|
40
46
|
hasBackendPermissiveCorsConfiguration,
|
|
41
47
|
hasBackendStringLiteralUnionEnumCandidate,
|
|
42
48
|
hasBackendHardDeleteWithoutSoftDelete,
|
|
43
49
|
hasBackendInconsistentErrorResponse,
|
|
44
50
|
hasBackendErrorPayloadWithSuccessStatus,
|
|
45
51
|
hasBackendControllerEntityResponse,
|
|
52
|
+
hasBackendControllerBusinessLogic,
|
|
53
|
+
hasBackendRepositoryBusinessLogic,
|
|
46
54
|
hasBackendRawThrowExpression,
|
|
55
|
+
hasBackendMissingGlobalValidationPipe,
|
|
56
|
+
hasBackendMissingHelmetSecurityHeaders,
|
|
47
57
|
hasEmptyCatchClause,
|
|
48
58
|
hasEvalCall,
|
|
49
59
|
hasExplicitAnyType,
|
|
@@ -715,6 +725,199 @@ test('hasBackendControllerEntityResponse detecta entidades expuestas por control
|
|
|
715
725
|
assert.equal(hasBackendControllerEntityResponse(dtoReturnTypeAst), false);
|
|
716
726
|
});
|
|
717
727
|
|
|
728
|
+
test('hasBackendControllerBusinessLogic detecta logica dentro de rutas controller NestJS', () => {
|
|
729
|
+
const controllerWithLogicAst = {
|
|
730
|
+
type: 'Program',
|
|
731
|
+
body: [
|
|
732
|
+
{
|
|
733
|
+
type: 'ClassDeclaration',
|
|
734
|
+
decorators: [
|
|
735
|
+
{
|
|
736
|
+
type: 'Decorator',
|
|
737
|
+
expression: {
|
|
738
|
+
type: 'CallExpression',
|
|
739
|
+
callee: { type: 'Identifier', name: 'Controller' },
|
|
740
|
+
arguments: [],
|
|
741
|
+
},
|
|
742
|
+
},
|
|
743
|
+
],
|
|
744
|
+
body: {
|
|
745
|
+
type: 'ClassBody',
|
|
746
|
+
body: [
|
|
747
|
+
{
|
|
748
|
+
type: 'ClassMethod',
|
|
749
|
+
key: { type: 'Identifier', name: 'createOrder' },
|
|
750
|
+
loc: { start: { line: 18 }, end: { line: 30 } },
|
|
751
|
+
decorators: [
|
|
752
|
+
{
|
|
753
|
+
type: 'Decorator',
|
|
754
|
+
expression: {
|
|
755
|
+
type: 'CallExpression',
|
|
756
|
+
callee: { type: 'Identifier', name: 'Post' },
|
|
757
|
+
arguments: [],
|
|
758
|
+
},
|
|
759
|
+
},
|
|
760
|
+
],
|
|
761
|
+
body: {
|
|
762
|
+
type: 'BlockStatement',
|
|
763
|
+
body: [
|
|
764
|
+
{
|
|
765
|
+
type: 'IfStatement',
|
|
766
|
+
test: { type: 'Identifier', name: 'invalid' },
|
|
767
|
+
consequent: { type: 'BlockStatement', body: [] },
|
|
768
|
+
},
|
|
769
|
+
],
|
|
770
|
+
},
|
|
771
|
+
},
|
|
772
|
+
],
|
|
773
|
+
},
|
|
774
|
+
},
|
|
775
|
+
],
|
|
776
|
+
};
|
|
777
|
+
const thinControllerAst = {
|
|
778
|
+
type: 'Program',
|
|
779
|
+
body: [
|
|
780
|
+
{
|
|
781
|
+
type: 'ClassDeclaration',
|
|
782
|
+
decorators: [
|
|
783
|
+
{
|
|
784
|
+
type: 'Decorator',
|
|
785
|
+
expression: {
|
|
786
|
+
type: 'CallExpression',
|
|
787
|
+
callee: { type: 'Identifier', name: 'Controller' },
|
|
788
|
+
arguments: [],
|
|
789
|
+
},
|
|
790
|
+
},
|
|
791
|
+
],
|
|
792
|
+
body: {
|
|
793
|
+
type: 'ClassBody',
|
|
794
|
+
body: [
|
|
795
|
+
{
|
|
796
|
+
type: 'ClassMethod',
|
|
797
|
+
key: { type: 'Identifier', name: 'createOrder' },
|
|
798
|
+
decorators: [
|
|
799
|
+
{
|
|
800
|
+
type: 'Decorator',
|
|
801
|
+
expression: {
|
|
802
|
+
type: 'CallExpression',
|
|
803
|
+
callee: { type: 'Identifier', name: 'Post' },
|
|
804
|
+
arguments: [],
|
|
805
|
+
},
|
|
806
|
+
},
|
|
807
|
+
],
|
|
808
|
+
body: {
|
|
809
|
+
type: 'BlockStatement',
|
|
810
|
+
body: [
|
|
811
|
+
{
|
|
812
|
+
type: 'ReturnStatement',
|
|
813
|
+
argument: {
|
|
814
|
+
type: 'CallExpression',
|
|
815
|
+
callee: {
|
|
816
|
+
type: 'MemberExpression',
|
|
817
|
+
object: {
|
|
818
|
+
type: 'MemberExpression',
|
|
819
|
+
object: { type: 'ThisExpression' },
|
|
820
|
+
property: { type: 'Identifier', name: 'ordersService' },
|
|
821
|
+
},
|
|
822
|
+
property: { type: 'Identifier', name: 'create' },
|
|
823
|
+
},
|
|
824
|
+
arguments: [],
|
|
825
|
+
},
|
|
826
|
+
},
|
|
827
|
+
],
|
|
828
|
+
},
|
|
829
|
+
},
|
|
830
|
+
],
|
|
831
|
+
},
|
|
832
|
+
},
|
|
833
|
+
],
|
|
834
|
+
};
|
|
835
|
+
|
|
836
|
+
assert.equal(hasBackendControllerBusinessLogic(controllerWithLogicAst), true);
|
|
837
|
+
assert.equal(hasBackendControllerBusinessLogic(thinControllerAst), false);
|
|
838
|
+
|
|
839
|
+
const match = findBackendControllerBusinessLogicMatch(controllerWithLogicAst);
|
|
840
|
+
assert.ok(match);
|
|
841
|
+
assert.deepEqual(match.lines, [18]);
|
|
842
|
+
assert.equal(match.primary_node, 'createOrder');
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
test('hasBackendRepositoryBusinessLogic detecta decisiones de negocio dentro de repositories', () => {
|
|
846
|
+
const repositoryWithLogicAst = {
|
|
847
|
+
type: 'Program',
|
|
848
|
+
body: [
|
|
849
|
+
{
|
|
850
|
+
type: 'ClassDeclaration',
|
|
851
|
+
id: { type: 'Identifier', name: 'OrdersRepository' },
|
|
852
|
+
body: {
|
|
853
|
+
type: 'ClassBody',
|
|
854
|
+
body: [
|
|
855
|
+
{
|
|
856
|
+
type: 'ClassMethod',
|
|
857
|
+
key: { type: 'Identifier', name: 'saveOrder' },
|
|
858
|
+
loc: { start: { line: 27 }, end: { line: 35 } },
|
|
859
|
+
body: {
|
|
860
|
+
type: 'BlockStatement',
|
|
861
|
+
body: [
|
|
862
|
+
{
|
|
863
|
+
type: 'IfStatement',
|
|
864
|
+
test: { type: 'Identifier', name: 'isPremium' },
|
|
865
|
+
consequent: { type: 'BlockStatement', body: [] },
|
|
866
|
+
},
|
|
867
|
+
],
|
|
868
|
+
},
|
|
869
|
+
},
|
|
870
|
+
],
|
|
871
|
+
},
|
|
872
|
+
},
|
|
873
|
+
],
|
|
874
|
+
};
|
|
875
|
+
const thinRepositoryAst = {
|
|
876
|
+
type: 'Program',
|
|
877
|
+
body: [
|
|
878
|
+
{
|
|
879
|
+
type: 'ClassDeclaration',
|
|
880
|
+
id: { type: 'Identifier', name: 'OrdersRepository' },
|
|
881
|
+
body: {
|
|
882
|
+
type: 'ClassBody',
|
|
883
|
+
body: [
|
|
884
|
+
{
|
|
885
|
+
type: 'ClassMethod',
|
|
886
|
+
key: { type: 'Identifier', name: 'findActiveOrdersByUserId' },
|
|
887
|
+
body: {
|
|
888
|
+
type: 'BlockStatement',
|
|
889
|
+
body: [
|
|
890
|
+
{
|
|
891
|
+
type: 'ReturnStatement',
|
|
892
|
+
argument: {
|
|
893
|
+
type: 'CallExpression',
|
|
894
|
+
callee: {
|
|
895
|
+
type: 'MemberExpression',
|
|
896
|
+
object: { type: 'Identifier', name: 'queryBuilder' },
|
|
897
|
+
property: { type: 'Identifier', name: 'where' },
|
|
898
|
+
},
|
|
899
|
+
arguments: [],
|
|
900
|
+
},
|
|
901
|
+
},
|
|
902
|
+
],
|
|
903
|
+
},
|
|
904
|
+
},
|
|
905
|
+
],
|
|
906
|
+
},
|
|
907
|
+
},
|
|
908
|
+
],
|
|
909
|
+
};
|
|
910
|
+
|
|
911
|
+
assert.equal(hasBackendRepositoryBusinessLogic(repositoryWithLogicAst), true);
|
|
912
|
+
assert.equal(hasBackendRepositoryBusinessLogic(thinRepositoryAst), false);
|
|
913
|
+
|
|
914
|
+
const match = findBackendRepositoryBusinessLogicMatch(repositoryWithLogicAst);
|
|
915
|
+
assert.ok(match);
|
|
916
|
+
assert.deepEqual(match.lines, [27]);
|
|
917
|
+
assert.equal(match.primary_node, 'OrdersRepository');
|
|
918
|
+
assert.deepEqual(match.related_nodes, ['saveOrder']);
|
|
919
|
+
});
|
|
920
|
+
|
|
718
921
|
test('hasBackendAnemicDomainModel detecta entidades de dominio anemicas', () => {
|
|
719
922
|
const anemicEntityAst = {
|
|
720
923
|
type: 'ClassDeclaration',
|
|
@@ -2758,6 +2961,164 @@ test('hasDtoNestedPropertyWithoutNestedValidation detecta DTOs anidados sin Vali
|
|
|
2758
2961
|
assert.deepEqual(match.related_nodes, ['address']);
|
|
2759
2962
|
});
|
|
2760
2963
|
|
|
2964
|
+
test('hasBackendMissingGlobalValidationPipe detecta bootstrap NestJS sin ValidationPipe estricto', () => {
|
|
2965
|
+
const missingPipeAst = {
|
|
2966
|
+
type: 'Program',
|
|
2967
|
+
body: [
|
|
2968
|
+
{
|
|
2969
|
+
type: 'ExpressionStatement',
|
|
2970
|
+
expression: {
|
|
2971
|
+
type: 'CallExpression',
|
|
2972
|
+
loc: { start: { line: 7 }, end: { line: 7 } },
|
|
2973
|
+
callee: {
|
|
2974
|
+
type: 'MemberExpression',
|
|
2975
|
+
object: { type: 'Identifier', name: 'NestFactory' },
|
|
2976
|
+
property: { type: 'Identifier', name: 'create' },
|
|
2977
|
+
},
|
|
2978
|
+
arguments: [],
|
|
2979
|
+
},
|
|
2980
|
+
},
|
|
2981
|
+
],
|
|
2982
|
+
};
|
|
2983
|
+
const unsafePipeAst = {
|
|
2984
|
+
type: 'Program',
|
|
2985
|
+
body: [
|
|
2986
|
+
{
|
|
2987
|
+
type: 'ExpressionStatement',
|
|
2988
|
+
expression: {
|
|
2989
|
+
type: 'CallExpression',
|
|
2990
|
+
loc: { start: { line: 11 }, end: { line: 11 } },
|
|
2991
|
+
callee: {
|
|
2992
|
+
type: 'MemberExpression',
|
|
2993
|
+
object: { type: 'Identifier', name: 'app' },
|
|
2994
|
+
property: { type: 'Identifier', name: 'useGlobalPipes' },
|
|
2995
|
+
},
|
|
2996
|
+
arguments: [
|
|
2997
|
+
{
|
|
2998
|
+
type: 'NewExpression',
|
|
2999
|
+
callee: { type: 'Identifier', name: 'ValidationPipe' },
|
|
3000
|
+
arguments: [],
|
|
3001
|
+
},
|
|
3002
|
+
],
|
|
3003
|
+
},
|
|
3004
|
+
},
|
|
3005
|
+
],
|
|
3006
|
+
};
|
|
3007
|
+
const strictPipeAst = {
|
|
3008
|
+
type: 'Program',
|
|
3009
|
+
body: [
|
|
3010
|
+
{
|
|
3011
|
+
type: 'ExpressionStatement',
|
|
3012
|
+
expression: {
|
|
3013
|
+
type: 'CallExpression',
|
|
3014
|
+
callee: {
|
|
3015
|
+
type: 'MemberExpression',
|
|
3016
|
+
object: { type: 'Identifier', name: 'NestFactory' },
|
|
3017
|
+
property: { type: 'Identifier', name: 'create' },
|
|
3018
|
+
},
|
|
3019
|
+
arguments: [],
|
|
3020
|
+
},
|
|
3021
|
+
},
|
|
3022
|
+
{
|
|
3023
|
+
type: 'ExpressionStatement',
|
|
3024
|
+
expression: {
|
|
3025
|
+
type: 'CallExpression',
|
|
3026
|
+
callee: {
|
|
3027
|
+
type: 'MemberExpression',
|
|
3028
|
+
object: { type: 'Identifier', name: 'app' },
|
|
3029
|
+
property: { type: 'Identifier', name: 'useGlobalPipes' },
|
|
3030
|
+
},
|
|
3031
|
+
arguments: [
|
|
3032
|
+
{
|
|
3033
|
+
type: 'NewExpression',
|
|
3034
|
+
callee: { type: 'Identifier', name: 'ValidationPipe' },
|
|
3035
|
+
arguments: [
|
|
3036
|
+
{
|
|
3037
|
+
type: 'ObjectExpression',
|
|
3038
|
+
properties: [
|
|
3039
|
+
{
|
|
3040
|
+
type: 'ObjectProperty',
|
|
3041
|
+
key: { type: 'Identifier', name: 'whitelist' },
|
|
3042
|
+
value: { type: 'BooleanLiteral', value: true },
|
|
3043
|
+
},
|
|
3044
|
+
],
|
|
3045
|
+
},
|
|
3046
|
+
],
|
|
3047
|
+
},
|
|
3048
|
+
],
|
|
3049
|
+
},
|
|
3050
|
+
},
|
|
3051
|
+
],
|
|
3052
|
+
};
|
|
3053
|
+
|
|
3054
|
+
assert.equal(hasBackendMissingGlobalValidationPipe(missingPipeAst), true);
|
|
3055
|
+
assert.deepEqual(findBackendMissingGlobalValidationPipeLines(missingPipeAst), [7]);
|
|
3056
|
+
assert.equal(hasBackendMissingGlobalValidationPipe(unsafePipeAst), true);
|
|
3057
|
+
assert.deepEqual(findBackendMissingGlobalValidationPipeLines(unsafePipeAst), [11]);
|
|
3058
|
+
assert.equal(hasBackendMissingGlobalValidationPipe(strictPipeAst), false);
|
|
3059
|
+
});
|
|
3060
|
+
|
|
3061
|
+
test('hasBackendMissingHelmetSecurityHeaders detecta bootstrap NestJS sin Helmet', () => {
|
|
3062
|
+
const missingHelmetAst = {
|
|
3063
|
+
type: 'Program',
|
|
3064
|
+
body: [
|
|
3065
|
+
{
|
|
3066
|
+
type: 'ExpressionStatement',
|
|
3067
|
+
expression: {
|
|
3068
|
+
type: 'CallExpression',
|
|
3069
|
+
loc: { start: { line: 9 }, end: { line: 9 } },
|
|
3070
|
+
callee: {
|
|
3071
|
+
type: 'MemberExpression',
|
|
3072
|
+
object: { type: 'Identifier', name: 'NestFactory' },
|
|
3073
|
+
property: { type: 'Identifier', name: 'create' },
|
|
3074
|
+
},
|
|
3075
|
+
arguments: [],
|
|
3076
|
+
},
|
|
3077
|
+
},
|
|
3078
|
+
],
|
|
3079
|
+
};
|
|
3080
|
+
const helmetAst = {
|
|
3081
|
+
type: 'Program',
|
|
3082
|
+
body: [
|
|
3083
|
+
{
|
|
3084
|
+
type: 'ExpressionStatement',
|
|
3085
|
+
expression: {
|
|
3086
|
+
type: 'CallExpression',
|
|
3087
|
+
loc: { start: { line: 11 }, end: { line: 11 } },
|
|
3088
|
+
callee: {
|
|
3089
|
+
type: 'MemberExpression',
|
|
3090
|
+
object: { type: 'Identifier', name: 'NestFactory' },
|
|
3091
|
+
property: { type: 'Identifier', name: 'create' },
|
|
3092
|
+
},
|
|
3093
|
+
arguments: [],
|
|
3094
|
+
},
|
|
3095
|
+
},
|
|
3096
|
+
{
|
|
3097
|
+
type: 'ExpressionStatement',
|
|
3098
|
+
expression: {
|
|
3099
|
+
type: 'CallExpression',
|
|
3100
|
+
callee: {
|
|
3101
|
+
type: 'MemberExpression',
|
|
3102
|
+
object: { type: 'Identifier', name: 'app' },
|
|
3103
|
+
property: { type: 'Identifier', name: 'use' },
|
|
3104
|
+
},
|
|
3105
|
+
arguments: [
|
|
3106
|
+
{
|
|
3107
|
+
type: 'CallExpression',
|
|
3108
|
+
callee: { type: 'Identifier', name: 'helmet' },
|
|
3109
|
+
arguments: [],
|
|
3110
|
+
},
|
|
3111
|
+
],
|
|
3112
|
+
},
|
|
3113
|
+
},
|
|
3114
|
+
],
|
|
3115
|
+
};
|
|
3116
|
+
|
|
3117
|
+
assert.equal(hasBackendMissingHelmetSecurityHeaders(missingHelmetAst), true);
|
|
3118
|
+
assert.deepEqual(findBackendMissingHelmetSecurityHeadersLines(missingHelmetAst), [9]);
|
|
3119
|
+
assert.equal(hasBackendMissingHelmetSecurityHeaders(helmetAst), false);
|
|
3120
|
+
});
|
|
3121
|
+
|
|
2761
3122
|
test('hasBackendControllerRouteWithoutGuard detecta rutas NestJS sin UseGuards', () => {
|
|
2762
3123
|
const unguardedRouteAst = {
|
|
2763
3124
|
type: 'Program',
|
|
@@ -2828,3 +3189,94 @@ test('hasBackendControllerRouteWithoutGuard detecta rutas NestJS sin UseGuards',
|
|
|
2828
3189
|
assert.equal(hasBackendControllerRouteWithoutGuard(classGuardedRouteAst), false);
|
|
2829
3190
|
assert.deepEqual(findBackendControllerRouteWithoutGuardLines(unguardedRouteAst), [12]);
|
|
2830
3191
|
});
|
|
3192
|
+
|
|
3193
|
+
test('hasBackendControllerRouteWithoutRoles detecta rutas NestJS protegidas sin RBAC explicito', () => {
|
|
3194
|
+
const guardedWithoutRolesAst = {
|
|
3195
|
+
type: 'Program',
|
|
3196
|
+
body: [
|
|
3197
|
+
{
|
|
3198
|
+
type: 'ClassDeclaration',
|
|
3199
|
+
decorators: [
|
|
3200
|
+
{
|
|
3201
|
+
type: 'Decorator',
|
|
3202
|
+
expression: { type: 'CallExpression', callee: { type: 'Identifier', name: 'Controller' }, arguments: [] },
|
|
3203
|
+
},
|
|
3204
|
+
{
|
|
3205
|
+
type: 'Decorator',
|
|
3206
|
+
expression: { type: 'CallExpression', callee: { type: 'Identifier', name: 'UseGuards' }, arguments: [] },
|
|
3207
|
+
},
|
|
3208
|
+
],
|
|
3209
|
+
body: {
|
|
3210
|
+
type: 'ClassBody',
|
|
3211
|
+
body: [
|
|
3212
|
+
{
|
|
3213
|
+
type: 'ClassMethod',
|
|
3214
|
+
key: { type: 'Identifier', name: 'deleteOrder' },
|
|
3215
|
+
loc: { start: { line: 31 }, end: { line: 33 } },
|
|
3216
|
+
decorators: [
|
|
3217
|
+
{
|
|
3218
|
+
type: 'Decorator',
|
|
3219
|
+
expression: { type: 'CallExpression', callee: { type: 'Identifier', name: 'Delete' }, arguments: [] },
|
|
3220
|
+
},
|
|
3221
|
+
],
|
|
3222
|
+
},
|
|
3223
|
+
],
|
|
3224
|
+
},
|
|
3225
|
+
},
|
|
3226
|
+
],
|
|
3227
|
+
};
|
|
3228
|
+
const methodRolesAst = {
|
|
3229
|
+
type: 'Program',
|
|
3230
|
+
body: [
|
|
3231
|
+
{
|
|
3232
|
+
type: 'ClassDeclaration',
|
|
3233
|
+
decorators: [
|
|
3234
|
+
{
|
|
3235
|
+
type: 'Decorator',
|
|
3236
|
+
expression: { type: 'CallExpression', callee: { type: 'Identifier', name: 'Controller' }, arguments: [] },
|
|
3237
|
+
},
|
|
3238
|
+
{
|
|
3239
|
+
type: 'Decorator',
|
|
3240
|
+
expression: { type: 'CallExpression', callee: { type: 'Identifier', name: 'UseGuards' }, arguments: [] },
|
|
3241
|
+
},
|
|
3242
|
+
],
|
|
3243
|
+
body: {
|
|
3244
|
+
type: 'ClassBody',
|
|
3245
|
+
body: [
|
|
3246
|
+
{
|
|
3247
|
+
type: 'ClassMethod',
|
|
3248
|
+
key: { type: 'Identifier', name: 'deleteOrder' },
|
|
3249
|
+
loc: { start: { line: 42 }, end: { line: 44 } },
|
|
3250
|
+
decorators: [
|
|
3251
|
+
{
|
|
3252
|
+
type: 'Decorator',
|
|
3253
|
+
expression: { type: 'CallExpression', callee: { type: 'Identifier', name: 'Roles' }, arguments: [] },
|
|
3254
|
+
},
|
|
3255
|
+
{
|
|
3256
|
+
type: 'Decorator',
|
|
3257
|
+
expression: { type: 'CallExpression', callee: { type: 'Identifier', name: 'Delete' }, arguments: [] },
|
|
3258
|
+
},
|
|
3259
|
+
],
|
|
3260
|
+
},
|
|
3261
|
+
],
|
|
3262
|
+
},
|
|
3263
|
+
},
|
|
3264
|
+
],
|
|
3265
|
+
};
|
|
3266
|
+
const unguardedRouteAst = {
|
|
3267
|
+
type: 'ClassMethod',
|
|
3268
|
+
key: { type: 'Identifier', name: 'publicHealth' },
|
|
3269
|
+
loc: { start: { line: 51 }, end: { line: 53 } },
|
|
3270
|
+
decorators: [
|
|
3271
|
+
{
|
|
3272
|
+
type: 'Decorator',
|
|
3273
|
+
expression: { type: 'CallExpression', callee: { type: 'Identifier', name: 'Get' }, arguments: [] },
|
|
3274
|
+
},
|
|
3275
|
+
],
|
|
3276
|
+
};
|
|
3277
|
+
|
|
3278
|
+
assert.equal(hasBackendControllerRouteWithoutRoles(guardedWithoutRolesAst), true);
|
|
3279
|
+
assert.deepEqual(findBackendControllerRouteWithoutRolesLines(guardedWithoutRolesAst), [31]);
|
|
3280
|
+
assert.equal(hasBackendControllerRouteWithoutRoles(methodRolesAst), false);
|
|
3281
|
+
assert.equal(hasBackendControllerRouteWithoutRoles(unguardedRouteAst), false);
|
|
3282
|
+
});
|
|
@@ -2155,7 +2155,7 @@ const isCorsPermissiveValue = (node: unknown): boolean => {
|
|
|
2155
2155
|
);
|
|
2156
2156
|
};
|
|
2157
2157
|
|
|
2158
|
-
const
|
|
2158
|
+
const validationPipeObjectPropertyName = (node: unknown): string | undefined => {
|
|
2159
2159
|
if (!isObject(node) || (node.type !== 'ObjectProperty' && node.type !== 'Property')) {
|
|
2160
2160
|
return undefined;
|
|
2161
2161
|
}
|
|
@@ -2766,6 +2766,7 @@ const nestJsClassDecoratorNames = new Set([
|
|
|
2766
2766
|
]);
|
|
2767
2767
|
const nestJsRouteDecoratorNames = new Set(['Delete', 'Get', 'Head', 'Options', 'Patch', 'Post', 'Put']);
|
|
2768
2768
|
const nestJsUseGuardsDecoratorNames = new Set(['UseGuards']);
|
|
2769
|
+
const nestJsRolesDecoratorNames = new Set(['Roles']);
|
|
2769
2770
|
|
|
2770
2771
|
const decoratorExpressionName = (node: unknown): string | undefined => {
|
|
2771
2772
|
if (!isObject(node)) {
|
|
@@ -2801,12 +2802,42 @@ const hasDecoratorFrom = (node: AstNode, allowedNames: ReadonlySet<string>): boo
|
|
|
2801
2802
|
});
|
|
2802
2803
|
};
|
|
2803
2804
|
|
|
2805
|
+
const decoratorCallArguments = (decorator: AstNode): readonly unknown[] => {
|
|
2806
|
+
const expression = decorator.expression;
|
|
2807
|
+
if (!isObject(expression) || expression.type !== 'CallExpression') {
|
|
2808
|
+
return [];
|
|
2809
|
+
}
|
|
2810
|
+
return Array.isArray(expression.arguments) ? expression.arguments : [];
|
|
2811
|
+
};
|
|
2812
|
+
|
|
2804
2813
|
const hasNestJsRouteDecorator = (node: AstNode): boolean =>
|
|
2805
2814
|
hasDecoratorFrom(node, nestJsRouteDecoratorNames);
|
|
2806
2815
|
|
|
2807
2816
|
const hasNestJsUseGuardsDecorator = (node: AstNode): boolean =>
|
|
2808
2817
|
hasDecoratorFrom(node, nestJsUseGuardsDecoratorNames);
|
|
2809
2818
|
|
|
2819
|
+
const isRolesSetMetadataDecorator = (decorator: AstNode): boolean => {
|
|
2820
|
+
const name = decoratorExpressionName(decorator.expression);
|
|
2821
|
+
if (name !== 'SetMetadata') {
|
|
2822
|
+
return false;
|
|
2823
|
+
}
|
|
2824
|
+
const [metadataKey] = decoratorCallArguments(decorator);
|
|
2825
|
+
return isObject(metadataKey) && metadataKey.type === 'StringLiteral' && metadataKey.value === 'roles';
|
|
2826
|
+
};
|
|
2827
|
+
|
|
2828
|
+
const hasNestJsRolesDecorator = (node: AstNode): boolean => {
|
|
2829
|
+
if (!Array.isArray(node.decorators)) {
|
|
2830
|
+
return false;
|
|
2831
|
+
}
|
|
2832
|
+
return node.decorators.some((decorator) => {
|
|
2833
|
+
if (!isObject(decorator)) {
|
|
2834
|
+
return false;
|
|
2835
|
+
}
|
|
2836
|
+
const name = decoratorExpressionName(decorator.expression);
|
|
2837
|
+
return (name !== undefined && nestJsRolesDecoratorNames.has(name)) || isRolesSetMetadataDecorator(decorator);
|
|
2838
|
+
});
|
|
2839
|
+
};
|
|
2840
|
+
|
|
2810
2841
|
const isClassLikeNode = (node: unknown): node is AstNode =>
|
|
2811
2842
|
isObject(node) && (node.type === 'ClassDeclaration' || node.type === 'ClassExpression');
|
|
2812
2843
|
|
|
@@ -2926,6 +2957,23 @@ export const findBackendControllerRouteWithoutGuardLines = (ast: unknown): reado
|
|
|
2926
2957
|
export const hasBackendControllerRouteWithoutGuard = (ast: unknown): boolean =>
|
|
2927
2958
|
findBackendControllerRouteWithoutGuardLines(ast).length > 0;
|
|
2928
2959
|
|
|
2960
|
+
export const findBackendControllerRouteWithoutRolesLines = (ast: unknown): readonly number[] => {
|
|
2961
|
+
return collectLineMatchesWithAncestors(ast, (node, ancestors) => {
|
|
2962
|
+
if (!isClassMethodLikeNode(node) || !hasNestJsRouteDecorator(node)) {
|
|
2963
|
+
return false;
|
|
2964
|
+
}
|
|
2965
|
+
const ownerClass = [...ancestors].reverse().find(isClassLikeNode);
|
|
2966
|
+
const hasGuard = hasNestJsUseGuardsDecorator(node) || (ownerClass !== undefined && hasNestJsUseGuardsDecorator(ownerClass));
|
|
2967
|
+
if (!hasGuard) {
|
|
2968
|
+
return false;
|
|
2969
|
+
}
|
|
2970
|
+
return !hasNestJsRolesDecorator(node) && (ownerClass === undefined || !hasNestJsRolesDecorator(ownerClass));
|
|
2971
|
+
});
|
|
2972
|
+
};
|
|
2973
|
+
|
|
2974
|
+
export const hasBackendControllerRouteWithoutRoles = (ast: unknown): boolean =>
|
|
2975
|
+
findBackendControllerRouteWithoutRolesLines(ast).length > 0;
|
|
2976
|
+
|
|
2929
2977
|
const persistenceMutationNames = new Set([
|
|
2930
2978
|
'create',
|
|
2931
2979
|
'delete',
|
|
@@ -3290,6 +3338,298 @@ export const findDtoNestedPropertyWithoutNestedValidationMatch = (
|
|
|
3290
3338
|
export const hasDtoNestedPropertyWithoutNestedValidation = (ast: unknown): boolean =>
|
|
3291
3339
|
findDtoNestedPropertyWithoutNestedValidationMatch(ast) !== undefined;
|
|
3292
3340
|
|
|
3341
|
+
const isValidationPipeCallee = (node: unknown): boolean => {
|
|
3342
|
+
return isObject(node) && node.type === 'Identifier' && node.name === 'ValidationPipe';
|
|
3343
|
+
};
|
|
3344
|
+
|
|
3345
|
+
const objectPropertyName = (node: unknown): string | undefined => {
|
|
3346
|
+
if (!isObject(node)) {
|
|
3347
|
+
return undefined;
|
|
3348
|
+
}
|
|
3349
|
+
if (isObject(node.key)) {
|
|
3350
|
+
return methodNameFromNode(node.key);
|
|
3351
|
+
}
|
|
3352
|
+
return undefined;
|
|
3353
|
+
};
|
|
3354
|
+
|
|
3355
|
+
const isBooleanTrueLiteral = (node: unknown): boolean => {
|
|
3356
|
+
return isObject(node) && node.type === 'BooleanLiteral' && node.value === true;
|
|
3357
|
+
};
|
|
3358
|
+
|
|
3359
|
+
const validationPipeOptionsEnableWhitelist = (node: unknown): boolean => {
|
|
3360
|
+
if (!isObject(node) || node.type !== 'ObjectExpression' || !Array.isArray(node.properties)) {
|
|
3361
|
+
return false;
|
|
3362
|
+
}
|
|
3363
|
+
return node.properties.some((property) => {
|
|
3364
|
+
return (
|
|
3365
|
+
isObject(property) &&
|
|
3366
|
+
validationPipeObjectPropertyName(property) === 'whitelist' &&
|
|
3367
|
+
isBooleanTrueLiteral(property.value)
|
|
3368
|
+
);
|
|
3369
|
+
});
|
|
3370
|
+
};
|
|
3371
|
+
|
|
3372
|
+
const isStrictValidationPipeExpression = (node: unknown): boolean => {
|
|
3373
|
+
if (!isObject(node)) {
|
|
3374
|
+
return false;
|
|
3375
|
+
}
|
|
3376
|
+
if (node.type !== 'NewExpression' && node.type !== 'CallExpression') {
|
|
3377
|
+
return false;
|
|
3378
|
+
}
|
|
3379
|
+
if (!isValidationPipeCallee(node.callee)) {
|
|
3380
|
+
return false;
|
|
3381
|
+
}
|
|
3382
|
+
const args = Array.isArray(node.arguments) ? node.arguments : [];
|
|
3383
|
+
return args.some(validationPipeOptionsEnableWhitelist);
|
|
3384
|
+
};
|
|
3385
|
+
|
|
3386
|
+
const isUseGlobalPipesCall = (node: AstNode): boolean => {
|
|
3387
|
+
return node.type === 'CallExpression' && callExpressionMemberName(node) === 'useGlobalPipes';
|
|
3388
|
+
};
|
|
3389
|
+
|
|
3390
|
+
const isStrictUseGlobalPipesCall = (node: AstNode): boolean => {
|
|
3391
|
+
if (!isUseGlobalPipesCall(node)) {
|
|
3392
|
+
return false;
|
|
3393
|
+
}
|
|
3394
|
+
const args = Array.isArray(node.arguments) ? node.arguments : [];
|
|
3395
|
+
return args.some(isStrictValidationPipeExpression);
|
|
3396
|
+
};
|
|
3397
|
+
|
|
3398
|
+
const isBackendValidationNestFactoryCreateCall = (node: AstNode): boolean => {
|
|
3399
|
+
if (node.type !== 'CallExpression') {
|
|
3400
|
+
return false;
|
|
3401
|
+
}
|
|
3402
|
+
const memberName = callExpressionMemberName(node);
|
|
3403
|
+
const objectName = callExpressionObjectName(node);
|
|
3404
|
+
return memberName === 'create' && objectName === 'NestFactory';
|
|
3405
|
+
};
|
|
3406
|
+
|
|
3407
|
+
export const findBackendMissingGlobalValidationPipeLines = (ast: unknown): readonly number[] => {
|
|
3408
|
+
const unsafeUseGlobalPipesLines = collectLineMatchesWithAncestors(
|
|
3409
|
+
ast,
|
|
3410
|
+
(node) => isUseGlobalPipesCall(node) && !isStrictUseGlobalPipesCall(node)
|
|
3411
|
+
);
|
|
3412
|
+
if (unsafeUseGlobalPipesLines.length > 0) {
|
|
3413
|
+
return unsafeUseGlobalPipesLines;
|
|
3414
|
+
}
|
|
3415
|
+
|
|
3416
|
+
if (!hasNode(ast, isBackendValidationNestFactoryCreateCall)) {
|
|
3417
|
+
return [];
|
|
3418
|
+
}
|
|
3419
|
+
if (hasNode(ast, isStrictUseGlobalPipesCall)) {
|
|
3420
|
+
return [];
|
|
3421
|
+
}
|
|
3422
|
+
return collectLineMatchesWithAncestors(ast, isBackendValidationNestFactoryCreateCall, { max: 1 });
|
|
3423
|
+
};
|
|
3424
|
+
|
|
3425
|
+
export const hasBackendMissingGlobalValidationPipe = (ast: unknown): boolean =>
|
|
3426
|
+
findBackendMissingGlobalValidationPipeLines(ast).length > 0;
|
|
3427
|
+
|
|
3428
|
+
const isHelmetCallee = (node: unknown): boolean => {
|
|
3429
|
+
return isObject(node) && node.type === 'Identifier' && node.name === 'helmet';
|
|
3430
|
+
};
|
|
3431
|
+
|
|
3432
|
+
const isHelmetCallExpression = (node: unknown): boolean => {
|
|
3433
|
+
return isObject(node) && node.type === 'CallExpression' && isHelmetCallee(node.callee);
|
|
3434
|
+
};
|
|
3435
|
+
|
|
3436
|
+
const isAppUseHelmetCall = (node: AstNode): boolean => {
|
|
3437
|
+
if (node.type !== 'CallExpression' || callExpressionMemberName(node) !== 'use') {
|
|
3438
|
+
return false;
|
|
3439
|
+
}
|
|
3440
|
+
const args = Array.isArray(node.arguments) ? node.arguments : [];
|
|
3441
|
+
return args.some(isHelmetCallExpression);
|
|
3442
|
+
};
|
|
3443
|
+
|
|
3444
|
+
export const findBackendMissingHelmetSecurityHeadersLines = (ast: unknown): readonly number[] => {
|
|
3445
|
+
if (!hasNode(ast, isBackendValidationNestFactoryCreateCall)) {
|
|
3446
|
+
return [];
|
|
3447
|
+
}
|
|
3448
|
+
if (hasNode(ast, isAppUseHelmetCall)) {
|
|
3449
|
+
return [];
|
|
3450
|
+
}
|
|
3451
|
+
return collectLineMatchesWithAncestors(ast, isBackendValidationNestFactoryCreateCall, { max: 1 });
|
|
3452
|
+
};
|
|
3453
|
+
|
|
3454
|
+
export const hasBackendMissingHelmetSecurityHeaders = (ast: unknown): boolean =>
|
|
3455
|
+
findBackendMissingHelmetSecurityHeadersLines(ast).length > 0;
|
|
3456
|
+
|
|
3457
|
+
const backendRouteDecoratorNames = new Set([
|
|
3458
|
+
'All',
|
|
3459
|
+
'Delete',
|
|
3460
|
+
'Get',
|
|
3461
|
+
'Head',
|
|
3462
|
+
'Options',
|
|
3463
|
+
'Patch',
|
|
3464
|
+
'Post',
|
|
3465
|
+
'Put',
|
|
3466
|
+
]);
|
|
3467
|
+
|
|
3468
|
+
const backendControllerBusinessLogicNodeTypes = new Set([
|
|
3469
|
+
'DoWhileStatement',
|
|
3470
|
+
'ForInStatement',
|
|
3471
|
+
'ForOfStatement',
|
|
3472
|
+
'ForStatement',
|
|
3473
|
+
'IfStatement',
|
|
3474
|
+
'SwitchStatement',
|
|
3475
|
+
'WhileStatement',
|
|
3476
|
+
]);
|
|
3477
|
+
|
|
3478
|
+
const hasBackendRouteDecorator = (node: AstNode): boolean => {
|
|
3479
|
+
if (!Array.isArray(node.decorators)) {
|
|
3480
|
+
return false;
|
|
3481
|
+
}
|
|
3482
|
+
return node.decorators.some((decorator) => {
|
|
3483
|
+
if (!isObject(decorator)) {
|
|
3484
|
+
return false;
|
|
3485
|
+
}
|
|
3486
|
+
const name = decoratorExpressionName(decorator.expression);
|
|
3487
|
+
return name !== undefined && backendRouteDecoratorNames.has(name);
|
|
3488
|
+
});
|
|
3489
|
+
};
|
|
3490
|
+
|
|
3491
|
+
const isBackendControllerRouteMethod = (node: AstNode): boolean => {
|
|
3492
|
+
return (
|
|
3493
|
+
(node.type === 'ClassMethod' ||
|
|
3494
|
+
node.type === 'ClassPrivateMethod' ||
|
|
3495
|
+
node.type === 'ObjectMethod') &&
|
|
3496
|
+
hasBackendRouteDecorator(node)
|
|
3497
|
+
);
|
|
3498
|
+
};
|
|
3499
|
+
|
|
3500
|
+
const isBackendControllerBusinessLogicNode = (node: AstNode): boolean => {
|
|
3501
|
+
if (typeof node.type === 'string' && backendControllerBusinessLogicNodeTypes.has(node.type)) {
|
|
3502
|
+
return true;
|
|
3503
|
+
}
|
|
3504
|
+
if (isPersistenceMutationCall(node)) {
|
|
3505
|
+
return true;
|
|
3506
|
+
}
|
|
3507
|
+
const objectName = callExpressionObjectName(node);
|
|
3508
|
+
const rootObjectName =
|
|
3509
|
+
node.type === 'CallExpression' && isObject(node.callee)
|
|
3510
|
+
? memberExpressionRootObjectName(node.callee)
|
|
3511
|
+
: undefined;
|
|
3512
|
+
return (
|
|
3513
|
+
(objectName !== undefined && /(?:repository|repo|prisma|supabase|model|collection)$/i.test(objectName)) ||
|
|
3514
|
+
(rootObjectName !== undefined && /(?:repository|repo|prisma|supabase|model|collection)$/i.test(rootObjectName))
|
|
3515
|
+
);
|
|
3516
|
+
};
|
|
3517
|
+
|
|
3518
|
+
export type TypeScriptBackendControllerBusinessLogicMatch = {
|
|
3519
|
+
lines: readonly number[];
|
|
3520
|
+
primary_node: string;
|
|
3521
|
+
related_nodes: readonly string[];
|
|
3522
|
+
why: string;
|
|
3523
|
+
impact: string;
|
|
3524
|
+
expected_fix: string;
|
|
3525
|
+
};
|
|
3526
|
+
|
|
3527
|
+
export const findBackendControllerBusinessLogicMatch = (
|
|
3528
|
+
ast: unknown
|
|
3529
|
+
): TypeScriptBackendControllerBusinessLogicMatch | undefined => {
|
|
3530
|
+
const match = findFirstNodeWithAncestors(ast, (node) => {
|
|
3531
|
+
if (!isBackendControllerRouteMethod(node)) {
|
|
3532
|
+
return false;
|
|
3533
|
+
}
|
|
3534
|
+
return hasDescendantNode(
|
|
3535
|
+
node,
|
|
3536
|
+
(nested) => nested !== node && isBackendControllerBusinessLogicNode(nested)
|
|
3537
|
+
);
|
|
3538
|
+
});
|
|
3539
|
+
|
|
3540
|
+
if (match === undefined) {
|
|
3541
|
+
return undefined;
|
|
3542
|
+
}
|
|
3543
|
+
|
|
3544
|
+
const methodName = methodNameFromNode(match.node.key) ?? 'anonymous controller route';
|
|
3545
|
+
const line = toPositiveLine(match.node);
|
|
3546
|
+
|
|
3547
|
+
return {
|
|
3548
|
+
lines: line === null ? [] : sortedUniqueLines([line]),
|
|
3549
|
+
primary_node: methodName,
|
|
3550
|
+
related_nodes: ['NestJS route handler', 'business logic in controller'],
|
|
3551
|
+
why: `El handler ${methodName} contiene logica de negocio o acceso a persistencia dentro del controller.`,
|
|
3552
|
+
impact:
|
|
3553
|
+
'El controller deja de ser una frontera HTTP delgada, mezcla routing con dominio/aplicacion y dificulta testabilidad y reutilizacion.',
|
|
3554
|
+
expected_fix:
|
|
3555
|
+
'Mueve la logica a un service/use case inyectado y deja el controller solo con routing, validacion de entrada y delegacion.',
|
|
3556
|
+
};
|
|
3557
|
+
};
|
|
3558
|
+
|
|
3559
|
+
export const findBackendControllerBusinessLogicLines = (ast: unknown): readonly number[] =>
|
|
3560
|
+
findBackendControllerBusinessLogicMatch(ast)?.lines ?? [];
|
|
3561
|
+
|
|
3562
|
+
export const hasBackendControllerBusinessLogic = (ast: unknown): boolean =>
|
|
3563
|
+
findBackendControllerBusinessLogicMatch(ast) !== undefined;
|
|
3564
|
+
|
|
3565
|
+
const isBackendRepositoryClassName = (name: string | undefined): boolean =>
|
|
3566
|
+
name !== undefined && /(?:Repository|Repo)$/i.test(name);
|
|
3567
|
+
|
|
3568
|
+
const isBackendRepositoryBusinessLogicMethod = (node: AstNode): boolean => {
|
|
3569
|
+
if (
|
|
3570
|
+
node.type !== 'ClassMethod' &&
|
|
3571
|
+
node.type !== 'ClassPrivateMethod' &&
|
|
3572
|
+
node.type !== 'ObjectMethod'
|
|
3573
|
+
) {
|
|
3574
|
+
return false;
|
|
3575
|
+
}
|
|
3576
|
+
return hasDescendantNode(node, (nested) => {
|
|
3577
|
+
return (
|
|
3578
|
+
nested !== node &&
|
|
3579
|
+
typeof nested.type === 'string' &&
|
|
3580
|
+
backendControllerBusinessLogicNodeTypes.has(nested.type)
|
|
3581
|
+
);
|
|
3582
|
+
});
|
|
3583
|
+
};
|
|
3584
|
+
|
|
3585
|
+
export type TypeScriptBackendRepositoryBusinessLogicMatch = {
|
|
3586
|
+
lines: readonly number[];
|
|
3587
|
+
primary_node: string;
|
|
3588
|
+
related_nodes: readonly string[];
|
|
3589
|
+
why: string;
|
|
3590
|
+
impact: string;
|
|
3591
|
+
expected_fix: string;
|
|
3592
|
+
};
|
|
3593
|
+
|
|
3594
|
+
export const findBackendRepositoryBusinessLogicMatch = (
|
|
3595
|
+
ast: unknown
|
|
3596
|
+
): TypeScriptBackendRepositoryBusinessLogicMatch | undefined => {
|
|
3597
|
+
const match = findFirstNodeWithAncestors(ast, (node) => {
|
|
3598
|
+
if (node.type !== 'ClassDeclaration' || !isBackendRepositoryClassName(classNameFromNode(node))) {
|
|
3599
|
+
return false;
|
|
3600
|
+
}
|
|
3601
|
+
const body = isObject(node.body) && Array.isArray(node.body.body) ? node.body.body : [];
|
|
3602
|
+
return body.some((member) => isObject(member) && isBackendRepositoryBusinessLogicMethod(member));
|
|
3603
|
+
});
|
|
3604
|
+
|
|
3605
|
+
if (match === undefined) {
|
|
3606
|
+
return undefined;
|
|
3607
|
+
}
|
|
3608
|
+
|
|
3609
|
+
const className = classNameFromNode(match.node) ?? 'Repository';
|
|
3610
|
+
const body = isObject(match.node.body) && Array.isArray(match.node.body.body) ? match.node.body.body : [];
|
|
3611
|
+
const method = body.find((member) => isObject(member) && isBackendRepositoryBusinessLogicMethod(member));
|
|
3612
|
+
const methodName = isObject(method) ? methodNameFromNode(method.key) : undefined;
|
|
3613
|
+
const line = isObject(method) ? toPositiveLine(method) : toPositiveLine(match.node);
|
|
3614
|
+
|
|
3615
|
+
return {
|
|
3616
|
+
lines: line === null ? [] : sortedUniqueLines([line]),
|
|
3617
|
+
primary_node: className,
|
|
3618
|
+
related_nodes: methodName === undefined ? ['repository method'] : [methodName],
|
|
3619
|
+
why: `El repositorio ${className} contiene ramas de decision dentro de un metodo de persistencia.`,
|
|
3620
|
+
impact:
|
|
3621
|
+
'El repositorio mezcla reglas de negocio con acceso a datos, rompiendo Clean Architecture y dificultando pruebas de casos de uso.',
|
|
3622
|
+
expected_fix:
|
|
3623
|
+
'Mueve la decision de negocio a un use case/service y deja el repositorio limitado a CRUD, queries expresivas y mapeo de datos.',
|
|
3624
|
+
};
|
|
3625
|
+
};
|
|
3626
|
+
|
|
3627
|
+
export const findBackendRepositoryBusinessLogicLines = (ast: unknown): readonly number[] =>
|
|
3628
|
+
findBackendRepositoryBusinessLogicMatch(ast)?.lines ?? [];
|
|
3629
|
+
|
|
3630
|
+
export const hasBackendRepositoryBusinessLogic = (ast: unknown): boolean =>
|
|
3631
|
+
findBackendRepositoryBusinessLogicMatch(ast) !== undefined;
|
|
3632
|
+
|
|
3293
3633
|
const isRecordTypeName = (node: unknown): boolean => {
|
|
3294
3634
|
return (
|
|
3295
3635
|
isObject(node) &&
|
|
@@ -499,8 +499,12 @@ const astDetectorRegistry: ReadonlyArray<ASTDetectorRegistryEntry> = [
|
|
|
499
499
|
{ detect: TS.hasBackendInconsistentErrorResponse, locateLines: TS.findBackendInconsistentErrorResponseLines, ruleId: 'heuristics.ts.backend.inconsistent-error-response.ast', code: 'HEURISTICS_BACKEND_INCONSISTENT_ERROR_RESPONSE_AST', message: 'AST heuristic detected backend error response without statusCode, message, timestamp and path.', pathCheck: isBackendTypeScriptPath },
|
|
500
500
|
{ detect: TS.hasBackendErrorPayloadWithSuccessStatus, locateLines: TS.findBackendErrorPayloadWithSuccessStatusLines, ruleId: 'heuristics.ts.backend.error-payload-success-status.ast', code: 'HEURISTICS_BACKEND_ERROR_PAYLOAD_SUCCESS_STATUS_AST', message: 'AST heuristic detected backend error payload sent with a success HTTP status.', pathCheck: isBackendTypeScriptPath },
|
|
501
501
|
{ detect: TS.hasBackendControllerEntityResponse, locateLines: TS.findBackendControllerEntityResponseLines, ruleId: 'heuristics.ts.backend.controller-entity-response.ast', code: 'HEURISTICS_BACKEND_CONTROLLER_ENTITY_RESPONSE_AST', message: 'AST heuristic detected backend controller exposing domain entities directly; return DTOs instead.', pathCheck: isBackendControllerTypeScriptPath },
|
|
502
|
+
{ detect: TS.hasBackendControllerBusinessLogic, locateLines: TS.findBackendControllerBusinessLogicLines, ruleId: 'heuristics.ts.backend.controller-business-logic.ast', code: 'HEURISTICS_BACKEND_CONTROLLER_BUSINESS_LOGIC_AST', message: 'AST heuristic detected business logic inside a NestJS controller route handler.', pathCheck: isBackendControllerTypeScriptPath },
|
|
503
|
+
{ detect: TS.hasBackendRepositoryBusinessLogic, locateLines: TS.findBackendRepositoryBusinessLogicLines, ruleId: 'heuristics.ts.backend.repository-business-logic.ast', code: 'HEURISTICS_BACKEND_REPOSITORY_BUSINESS_LOGIC_AST', message: 'AST heuristic detected business decision logic inside a backend repository.', pathCheck: isBackendTypeScriptPath },
|
|
502
504
|
{ detect: TS.hasBackendAnemicDomainModel, ruleId: 'heuristics.ts.backend.anemic-domain-model.ast', code: 'HEURISTICS_BACKEND_ANEMIC_DOMAIN_MODEL_AST', message: 'AST heuristic detected an anemic backend domain entity without business behavior.', pathCheck: isBackendDomainEntityPath },
|
|
503
505
|
{ detect: TS.hasBackendPermissiveCorsConfiguration, locateLines: TS.findBackendPermissiveCorsConfigurationLines, ruleId: 'heuristics.ts.backend.permissive-cors.ast', code: 'HEURISTICS_BACKEND_PERMISSIVE_CORS_AST', message: 'AST heuristic detected permissive backend CORS configuration; restrict allowed origins explicitly.', pathCheck: isBackendTypeScriptPath },
|
|
506
|
+
{ detect: TS.hasBackendMissingHelmetSecurityHeaders, locateLines: TS.findBackendMissingHelmetSecurityHeadersLines, ruleId: 'heuristics.ts.backend.missing-helmet-security-headers.ast', code: 'HEURISTICS_BACKEND_MISSING_HELMET_SECURITY_HEADERS_AST', message: 'AST heuristic detected NestJS bootstrap without Helmet security headers middleware.', pathCheck: isBackendTypeScriptPath },
|
|
507
|
+
{ detect: TS.hasBackendMissingGlobalValidationPipe, locateLines: TS.findBackendMissingGlobalValidationPipeLines, ruleId: 'heuristics.ts.backend.missing-global-validation-pipe.ast', code: 'HEURISTICS_BACKEND_MISSING_GLOBAL_VALIDATION_PIPE_AST', message: 'AST heuristic detected NestJS bootstrap without global ValidationPipe whitelist.', pathCheck: isBackendTypeScriptPath },
|
|
504
508
|
{ detect: TS.hasBackendStringLiteralUnionEnumCandidate, locateLines: TS.findBackendStringLiteralUnionEnumCandidateLines, ruleId: 'heuristics.ts.backend.string-literal-union-enum.ast', code: 'HEURISTICS_BACKEND_STRING_LITERAL_UNION_ENUM_AST', message: 'AST heuristic detected fixed backend string literal union; use an enum or centralized typed constants.', pathCheck: isBackendTypeScriptPath },
|
|
505
509
|
{ detect: TS.hasEvalCall, ruleId: 'heuristics.ts.eval.ast', code: 'HEURISTICS_EVAL_AST', message: 'AST heuristic detected eval usage.' },
|
|
506
510
|
{ detect: TS.hasFunctionConstructorUsage, ruleId: 'heuristics.ts.function-constructor.ast', code: 'HEURISTICS_FUNCTION_CONSTRUCTOR_AST', message: 'AST heuristic detected Function constructor usage.' },
|
|
@@ -527,6 +531,7 @@ const astDetectorRegistry: ReadonlyArray<ASTDetectorRegistryEntry> = [
|
|
|
527
531
|
{ detect: TS.hasConcreteDependencyInstantiation, ruleId: 'heuristics.ts.solid.dip.concrete-instantiation.ast', code: 'HEURISTICS_SOLID_DIP_CONCRETE_INSTANTIATION_AST', message: 'AST heuristic detected DIP risk: direct instantiation of concrete framework dependency.', pathCheck: isTypeScriptDomainOrApplicationPath },
|
|
528
532
|
{ detect: TS.hasNestJsConstructorDependencyWithoutDecorator, ruleId: 'heuristics.ts.nestjs.constructor-di-without-decorator.ast', code: 'HEURISTICS_NESTJS_CONSTRUCTOR_DI_WITHOUT_DECORATOR_AST', message: 'AST heuristic detected NestJS constructor dependency injection without an explicit class decorator.' },
|
|
529
533
|
{ detect: TS.hasBackendControllerRouteWithoutGuard, locateLines: TS.findBackendControllerRouteWithoutGuardLines, ruleId: 'heuristics.ts.backend.controller-route-without-guard.ast', code: 'HEURISTICS_BACKEND_CONTROLLER_ROUTE_WITHOUT_GUARD_AST', message: 'AST heuristic detected NestJS controller route without UseGuards at method or class level.', pathCheck: isBackendTypeScriptPath },
|
|
534
|
+
{ detect: TS.hasBackendControllerRouteWithoutRoles, locateLines: TS.findBackendControllerRouteWithoutRolesLines, ruleId: 'heuristics.ts.backend.controller-route-without-roles.ast', code: 'HEURISTICS_BACKEND_CONTROLLER_ROUTE_WITHOUT_ROLES_AST', message: 'AST heuristic detected guarded NestJS controller route without Roles/SetMetadata roles authorization.', pathCheck: isBackendTypeScriptPath },
|
|
530
535
|
{ detect: TS.hasPersistenceMutationWithoutAuditEvent, ruleId: 'heuristics.ts.backend.persistence-mutation-without-audit-event.ast', code: 'HEURISTICS_BACKEND_PERSISTENCE_MUTATION_WITHOUT_AUDIT_EVENT_AST', message: 'AST heuristic detected backend persistence mutation without audit log or domain event.' },
|
|
531
536
|
{ detect: TS.hasBackendHardDeleteWithoutSoftDelete, locateLines: TS.findBackendHardDeleteWithoutSoftDeleteLines, ruleId: 'heuristics.ts.backend.hard-delete-without-soft-delete.ast', code: 'HEURISTICS_BACKEND_HARD_DELETE_WITHOUT_SOFT_DELETE_AST', message: 'AST heuristic detected backend physical delete; use soft delete with deletedAt/deleted_at.', pathCheck: isBackendTypeScriptPath },
|
|
532
537
|
{ detect: TS.hasDtoPropertyWithoutValidation, ruleId: 'heuristics.ts.backend.dto-property-without-validation.ast', code: 'HEURISTICS_BACKEND_DTO_PROPERTY_WITHOUT_VALIDATION_AST', message: 'AST heuristic detected DTO property without class-validator/class-transformer decorator.' },
|
|
@@ -733,8 +738,8 @@ const textDetectorRegistry: ReadonlyArray<TextDetectorRegistryEntry> = [
|
|
|
733
738
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftTaskDetachedUsage, ruleId: 'heuristics.ios.task-detached.ast', code: 'HEURISTICS_IOS_TASK_DETACHED_AST', message: 'AST heuristic detected Task.detached usage.' },
|
|
734
739
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftAsyncWithoutAwaitUsage, ruleId: 'heuristics.ios.concurrency.async-without-await.ast', code: 'HEURISTICS_IOS_CONCURRENCY_ASYNC_WITHOUT_AWAIT_AST', message: 'AST heuristic detected a private async function without await; remove async unless a protocol/override boundary requires it.' },
|
|
735
740
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftEmptyCatchUsage, ruleId: 'heuristics.ios.error.empty-catch.ast', code: 'HEURISTICS_IOS_ERROR_EMPTY_CATCH_AST', message: 'AST heuristic detected an empty Swift catch block; handle, log, or propagate the error.' },
|
|
736
|
-
{ platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftOnAppearTaskUsage, ruleId: 'heuristics.ios.swiftui.onappear-task.ast', code: 'HEURISTICS_IOS_SWIFTUI_ONAPPEAR_TASK_AST', message: 'AST heuristic detected Task launched from SwiftUI onAppear; .task/.task(id:) provides lifecycle-aware cancellation.' },
|
|
737
|
-
{ platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftOnChangeTaskUsage, ruleId: 'heuristics.ios.swiftui.onchange-task.ast', code: 'HEURISTICS_IOS_SWIFTUI_ONCHANGE_TASK_AST', message: 'AST heuristic detected Task launched from SwiftUI onChange; .task(id:) provides lifecycle-aware cancellation for value-dependent async work.' },
|
|
741
|
+
{ platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftOnAppearTaskUsage, locateLines: TextIOS.collectSwiftOnAppearTaskLines, primaryNode: (lines) => ({ kind: 'call', name: 'Task launched inside SwiftUI onAppear', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: .task { ... } on the view', lines }], why: 'A Task launched from onAppear is not owned by the SwiftUI view lifecycle in the same way as .task.', impact: 'Async work can outlive view disappearance or require manual cancellation, and the gate must point to the exact Task line instead of blocking the whole file.', expected_fix: 'Move the async work from .onAppear { Task { ... } } into .task { ... } so SwiftUI owns automatic cancellation. Keep onAppear only for synchronous side effects such as analytics.', ruleId: 'heuristics.ios.swiftui.onappear-task.ast', code: 'HEURISTICS_IOS_SWIFTUI_ONAPPEAR_TASK_AST', message: 'AST heuristic detected Task launched from SwiftUI onAppear; .task/.task(id:) provides lifecycle-aware cancellation.' },
|
|
742
|
+
{ platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftOnChangeTaskUsage, locateLines: TextIOS.collectSwiftOnChangeTaskLines, primaryNode: (lines) => ({ kind: 'call', name: 'Task launched inside SwiftUI onChange', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: .task(id:) for value-dependent async work', lines }], why: 'A Task launched from onChange makes value-dependent async work manual instead of tying cancellation to the changing value.', impact: 'Search, load or refresh work can race after state changes because cancellation is not expressed through SwiftUI .task(id:).', expected_fix: 'Move value-dependent async work from .onChange { Task { ... } } into .task(id: value) { ... }. Keep onChange only for synchronous derivations or analytics.', ruleId: 'heuristics.ios.swiftui.onchange-task.ast', code: 'HEURISTICS_IOS_SWIFTUI_ONCHANGE_TASK_AST', message: 'AST heuristic detected Task launched from SwiftUI onChange; .task(id:) provides lifecycle-aware cancellation for value-dependent async work.' },
|
|
738
743
|
{ platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftOnChangeReadonlyVarUsage, locateLines: TextIOS.collectSwiftOnChangeReadonlyVarLines, primaryNode: (lines) => ({ kind: 'property', name: 'var declared inside SwiftUI onChange closure', lines }), relatedNodes: (lines) => [{ kind: 'property', name: 'replacement: let for read-only derived value inside onChange', lines }], why: 'A local var inside onChange hides whether the closure is deriving a read-only value or mutating state as part of a reactive update.', impact: 'Reactive closures become harder to audit because unnecessary mutability can mask accidental state changes and value-dependent side effects.', expected_fix: 'Use let for read-only derived values inside onChange. Keep var only when the local value is intentionally mutated and extract complex mutation out of the view closure.', ruleId: 'heuristics.ios.swiftui.onchange-readonly-var.ast', code: 'HEURISTICS_IOS_SWIFTUI_ONCHANGE_READONLY_VAR_AST', message: 'AST heuristic detected local var inside SwiftUI onChange; prefer let for read-only derived values.' },
|
|
739
744
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftStrongDelegateReferenceUsage, ruleId: 'heuristics.ios.memory.strong-delegate.ast', code: 'HEURISTICS_IOS_MEMORY_STRONG_DELEGATE_AST', message: 'AST heuristic detected a strong delegate/dataSource reference; weak delegates remain the preferred baseline to avoid retain cycles.' },
|
|
740
745
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftStrongSelfEscapingClosureUsage, ruleId: 'heuristics.ios.memory.strong-self-escaping-closure.ast', code: 'HEURISTICS_IOS_MEMORY_STRONG_SELF_ESCAPING_CLOSURE_AST', message: 'AST heuristic detected strong self capture in an escaping iOS closure; weak or unowned captures remain the preferred baseline when ownership is not explicit.' },
|
|
@@ -3,7 +3,7 @@ import test from 'node:test';
|
|
|
3
3
|
import { typescriptRules } from './typescript';
|
|
4
4
|
|
|
5
5
|
test('typescriptRules define reglas heurísticas locked para plataforma generic', () => {
|
|
6
|
-
assert.equal(typescriptRules.length,
|
|
6
|
+
assert.equal(typescriptRules.length, 52);
|
|
7
7
|
|
|
8
8
|
const ids = typescriptRules.map((rule) => rule.id);
|
|
9
9
|
assert.deepEqual(ids, [
|
|
@@ -49,10 +49,15 @@ test('typescriptRules define reglas heurísticas locked para plataforma generic'
|
|
|
49
49
|
'heuristics.ts.solid.dip.concrete-instantiation.ast',
|
|
50
50
|
'heuristics.ts.nestjs.constructor-di-without-decorator.ast',
|
|
51
51
|
'heuristics.ts.backend.controller-route-without-guard.ast',
|
|
52
|
+
'heuristics.ts.backend.controller-route-without-roles.ast',
|
|
52
53
|
'heuristics.ts.backend.persistence-mutation-without-audit-event.ast',
|
|
53
54
|
'heuristics.ts.backend.hard-delete-without-soft-delete.ast',
|
|
54
55
|
'heuristics.ts.backend.dto-property-without-validation.ast',
|
|
55
56
|
'heuristics.ts.backend.dto-nested-property-without-nested-validation.ast',
|
|
57
|
+
'heuristics.ts.backend.missing-global-validation-pipe.ast',
|
|
58
|
+
'heuristics.ts.backend.missing-helmet-security-headers.ast',
|
|
59
|
+
'heuristics.ts.backend.controller-business-logic.ast',
|
|
60
|
+
'heuristics.ts.backend.repository-business-logic.ast',
|
|
56
61
|
'heuristics.ts.god-class-large-class.ast',
|
|
57
62
|
]);
|
|
58
63
|
|
|
@@ -169,6 +174,10 @@ test('typescriptRules define reglas heurísticas locked para plataforma generic'
|
|
|
169
174
|
byId.get('heuristics.ts.backend.controller-route-without-guard.ast')?.then.code,
|
|
170
175
|
'HEURISTICS_BACKEND_CONTROLLER_ROUTE_WITHOUT_GUARD_AST'
|
|
171
176
|
);
|
|
177
|
+
assert.equal(
|
|
178
|
+
byId.get('heuristics.ts.backend.controller-route-without-roles.ast')?.then.code,
|
|
179
|
+
'HEURISTICS_BACKEND_CONTROLLER_ROUTE_WITHOUT_ROLES_AST'
|
|
180
|
+
);
|
|
172
181
|
assert.equal(
|
|
173
182
|
byId.get('heuristics.ts.backend.persistence-mutation-without-audit-event.ast')?.then.code,
|
|
174
183
|
'HEURISTICS_BACKEND_PERSISTENCE_MUTATION_WITHOUT_AUDIT_EVENT_AST'
|
|
@@ -185,6 +194,10 @@ test('typescriptRules define reglas heurísticas locked para plataforma generic'
|
|
|
185
194
|
byId.get('heuristics.ts.backend.dto-nested-property-without-nested-validation.ast')?.then.code,
|
|
186
195
|
'HEURISTICS_BACKEND_DTO_NESTED_PROPERTY_WITHOUT_NESTED_VALIDATION_AST'
|
|
187
196
|
);
|
|
197
|
+
assert.equal(
|
|
198
|
+
byId.get('heuristics.ts.backend.missing-helmet-security-headers.ast')?.then.code,
|
|
199
|
+
'HEURISTICS_BACKEND_MISSING_HELMET_SECURITY_HEADERS_AST'
|
|
200
|
+
);
|
|
188
201
|
assert.equal(
|
|
189
202
|
byId.get('heuristics.ts.god-class-large-class.ast')?.then.code,
|
|
190
203
|
'HEURISTICS_GOD_CLASS_LARGE_CLASS_AST'
|
|
@@ -195,6 +208,7 @@ test('typescriptRules define reglas heurísticas locked para plataforma generic'
|
|
|
195
208
|
if (
|
|
196
209
|
rule.id === 'heuristics.ts.god-class-large-class.ast' ||
|
|
197
210
|
rule.id === 'heuristics.ts.backend.controller-route-without-guard.ast' ||
|
|
211
|
+
rule.id === 'heuristics.ts.backend.controller-route-without-roles.ast' ||
|
|
198
212
|
rule.id === 'heuristics.ts.backend.persistence-mutation-without-audit-event.ast' ||
|
|
199
213
|
rule.id === 'heuristics.ts.backend.hard-delete-without-soft-delete.ast' ||
|
|
200
214
|
rule.id === 'heuristics.ts.backend.dto-property-without-validation.ast' ||
|
|
@@ -211,6 +225,10 @@ test('typescriptRules define reglas heurísticas locked para plataforma generic'
|
|
|
211
225
|
rule.id === 'heuristics.ts.backend.controller-entity-response.ast' ||
|
|
212
226
|
rule.id === 'heuristics.ts.backend.anemic-domain-model.ast' ||
|
|
213
227
|
rule.id === 'heuristics.ts.backend.permissive-cors.ast' ||
|
|
228
|
+
rule.id === 'heuristics.ts.backend.missing-global-validation-pipe.ast' ||
|
|
229
|
+
rule.id === 'heuristics.ts.backend.missing-helmet-security-headers.ast' ||
|
|
230
|
+
rule.id === 'heuristics.ts.backend.controller-business-logic.ast' ||
|
|
231
|
+
rule.id === 'heuristics.ts.backend.repository-business-logic.ast' ||
|
|
214
232
|
rule.id === 'heuristics.ts.backend.string-literal-union-enum.ast' ||
|
|
215
233
|
rule.id === 'heuristics.ts.sensitive-token-in-url.ast'
|
|
216
234
|
) {
|
|
@@ -764,6 +764,25 @@ export const typescriptRules: RuleSet = [
|
|
|
764
764
|
code: 'HEURISTICS_BACKEND_CONTROLLER_ROUTE_WITHOUT_GUARD_AST',
|
|
765
765
|
},
|
|
766
766
|
},
|
|
767
|
+
{
|
|
768
|
+
id: 'heuristics.ts.backend.controller-route-without-roles.ast',
|
|
769
|
+
description:
|
|
770
|
+
'Detects guarded NestJS controller route handlers without an explicit Roles decorator or roles metadata.',
|
|
771
|
+
severity: 'ERROR',
|
|
772
|
+
platform: 'generic',
|
|
773
|
+
locked: true,
|
|
774
|
+
when: {
|
|
775
|
+
kind: 'Heuristic',
|
|
776
|
+
where: {
|
|
777
|
+
ruleId: 'heuristics.ts.backend.controller-route-without-roles.ast',
|
|
778
|
+
},
|
|
779
|
+
},
|
|
780
|
+
then: {
|
|
781
|
+
kind: 'Finding',
|
|
782
|
+
message: 'AST heuristic detected guarded NestJS controller route without role metadata.',
|
|
783
|
+
code: 'HEURISTICS_BACKEND_CONTROLLER_ROUTE_WITHOUT_ROLES_AST',
|
|
784
|
+
},
|
|
785
|
+
},
|
|
767
786
|
{
|
|
768
787
|
id: 'heuristics.ts.backend.persistence-mutation-without-audit-event.ast',
|
|
769
788
|
description:
|
|
@@ -840,6 +859,86 @@ export const typescriptRules: RuleSet = [
|
|
|
840
859
|
code: 'HEURISTICS_BACKEND_DTO_NESTED_PROPERTY_WITHOUT_NESTED_VALIDATION_AST',
|
|
841
860
|
},
|
|
842
861
|
},
|
|
862
|
+
{
|
|
863
|
+
id: 'heuristics.ts.backend.missing-global-validation-pipe.ast',
|
|
864
|
+
description:
|
|
865
|
+
'Detects NestJS bootstrap code without a strict global ValidationPipe configured with whitelist=true.',
|
|
866
|
+
severity: 'ERROR',
|
|
867
|
+
platform: 'generic',
|
|
868
|
+
locked: true,
|
|
869
|
+
when: {
|
|
870
|
+
kind: 'Heuristic',
|
|
871
|
+
where: {
|
|
872
|
+
ruleId: 'heuristics.ts.backend.missing-global-validation-pipe.ast',
|
|
873
|
+
},
|
|
874
|
+
},
|
|
875
|
+
then: {
|
|
876
|
+
kind: 'Finding',
|
|
877
|
+
message:
|
|
878
|
+
'AST heuristic detected NestJS bootstrap without global ValidationPipe whitelist.',
|
|
879
|
+
code: 'HEURISTICS_BACKEND_MISSING_GLOBAL_VALIDATION_PIPE_AST',
|
|
880
|
+
},
|
|
881
|
+
},
|
|
882
|
+
{
|
|
883
|
+
id: 'heuristics.ts.backend.missing-helmet-security-headers.ast',
|
|
884
|
+
description:
|
|
885
|
+
'Detects NestJS bootstrap code without Helmet middleware for security headers.',
|
|
886
|
+
severity: 'ERROR',
|
|
887
|
+
platform: 'generic',
|
|
888
|
+
locked: true,
|
|
889
|
+
when: {
|
|
890
|
+
kind: 'Heuristic',
|
|
891
|
+
where: {
|
|
892
|
+
ruleId: 'heuristics.ts.backend.missing-helmet-security-headers.ast',
|
|
893
|
+
},
|
|
894
|
+
},
|
|
895
|
+
then: {
|
|
896
|
+
kind: 'Finding',
|
|
897
|
+
message:
|
|
898
|
+
'AST heuristic detected NestJS bootstrap without Helmet security headers middleware.',
|
|
899
|
+
code: 'HEURISTICS_BACKEND_MISSING_HELMET_SECURITY_HEADERS_AST',
|
|
900
|
+
},
|
|
901
|
+
},
|
|
902
|
+
{
|
|
903
|
+
id: 'heuristics.ts.backend.controller-business-logic.ast',
|
|
904
|
+
description:
|
|
905
|
+
'Detects NestJS controller route handlers that contain business logic or direct persistence access instead of delegating to services/use cases.',
|
|
906
|
+
severity: 'ERROR',
|
|
907
|
+
platform: 'generic',
|
|
908
|
+
locked: true,
|
|
909
|
+
when: {
|
|
910
|
+
kind: 'Heuristic',
|
|
911
|
+
where: {
|
|
912
|
+
ruleId: 'heuristics.ts.backend.controller-business-logic.ast',
|
|
913
|
+
},
|
|
914
|
+
},
|
|
915
|
+
then: {
|
|
916
|
+
kind: 'Finding',
|
|
917
|
+
message:
|
|
918
|
+
'AST heuristic detected business logic inside a NestJS controller route handler.',
|
|
919
|
+
code: 'HEURISTICS_BACKEND_CONTROLLER_BUSINESS_LOGIC_AST',
|
|
920
|
+
},
|
|
921
|
+
},
|
|
922
|
+
{
|
|
923
|
+
id: 'heuristics.ts.backend.repository-business-logic.ast',
|
|
924
|
+
description:
|
|
925
|
+
'Detects backend repository methods that contain business decision branches instead of staying limited to CRUD, queries and data mapping.',
|
|
926
|
+
severity: 'ERROR',
|
|
927
|
+
platform: 'generic',
|
|
928
|
+
locked: true,
|
|
929
|
+
when: {
|
|
930
|
+
kind: 'Heuristic',
|
|
931
|
+
where: {
|
|
932
|
+
ruleId: 'heuristics.ts.backend.repository-business-logic.ast',
|
|
933
|
+
},
|
|
934
|
+
},
|
|
935
|
+
then: {
|
|
936
|
+
kind: 'Finding',
|
|
937
|
+
message:
|
|
938
|
+
'AST heuristic detected business decision logic inside a backend repository.',
|
|
939
|
+
code: 'HEURISTICS_BACKEND_REPOSITORY_BUSINESS_LOGIC_AST',
|
|
940
|
+
},
|
|
941
|
+
},
|
|
843
942
|
{
|
|
844
943
|
id: 'heuristics.ts.god-class-large-class.ast',
|
|
845
944
|
description: 'Detects God Class candidates when one class mixes multiple responsibility nodes.',
|
|
@@ -813,6 +813,18 @@ const registryByRuleId: Record<string, SkillsDetectorBinding> = {
|
|
|
813
813
|
heuristicDetector('typescript.backend.dto-nested-property-without-nested-validation', [
|
|
814
814
|
'heuristics.ts.backend.dto-nested-property-without-nested-validation.ast',
|
|
815
815
|
]),
|
|
816
|
+
'skills.backend.guideline.backend.pipes-para-validacio-n-global-validationpipe-en-main-ts':
|
|
817
|
+
heuristicDetector('typescript.backend.missing-global-validation-pipe', [
|
|
818
|
+
'heuristics.ts.backend.missing-global-validation-pipe.ast',
|
|
819
|
+
]),
|
|
820
|
+
'skills.backend.guideline.backend.validationpipe-global-en-main-ts-con-whitelist-true':
|
|
821
|
+
heuristicDetector('typescript.backend.missing-global-validation-pipe', [
|
|
822
|
+
'heuristics.ts.backend.missing-global-validation-pipe.ast',
|
|
823
|
+
]),
|
|
824
|
+
'skills.backend.guideline.backend.helmet-security-headers':
|
|
825
|
+
heuristicDetector('typescript.backend.missing-helmet-security-headers', [
|
|
826
|
+
'heuristics.ts.backend.missing-helmet-security-headers.ast',
|
|
827
|
+
]),
|
|
816
828
|
'skills.backend.guideline.backend.guards-en-todas-las-rutas-protegidas-useguards-jwtauthguard':
|
|
817
829
|
heuristicDetector('typescript.backend.controller-route-without-guard', [
|
|
818
830
|
'heuristics.ts.backend.controller-route-without-guard.ast',
|
|
@@ -821,6 +833,10 @@ const registryByRuleId: Record<string, SkillsDetectorBinding> = {
|
|
|
821
833
|
heuristicDetector('typescript.backend.controller-route-without-guard', [
|
|
822
834
|
'heuristics.ts.backend.controller-route-without-guard.ast',
|
|
823
835
|
]),
|
|
836
|
+
'skills.backend.guideline.backend.role-based-access-control-roles-admin-user':
|
|
837
|
+
heuristicDetector('typescript.backend.controller-route-without-roles', [
|
|
838
|
+
'heuristics.ts.backend.controller-route-without-roles.ast',
|
|
839
|
+
]),
|
|
824
840
|
'skills.backend.guideline.backend.callback-hell-usar-async-await': heuristicDetector(
|
|
825
841
|
'typescript.callback-hell',
|
|
826
842
|
['heuristics.ts.callback-hell.ast']
|
|
@@ -898,6 +914,22 @@ const registryByRuleId: Record<string, SkillsDetectorBinding> = {
|
|
|
898
914
|
heuristicDetector('typescript.backend.controller-entity-response', [
|
|
899
915
|
'heuristics.ts.backend.controller-entity-response.ast',
|
|
900
916
|
]),
|
|
917
|
+
'skills.backend.guideline.backend.controllers-delgados-solo-routing-y-validacio-n-lo-gica-en-servicios':
|
|
918
|
+
heuristicDetector('typescript.backend.controller-business-logic', [
|
|
919
|
+
'heuristics.ts.backend.controller-business-logic.ast',
|
|
920
|
+
]),
|
|
921
|
+
'skills.backend.guideline.backend.lo-gica-en-controllers-mover-a-servicios-use-cases':
|
|
922
|
+
heuristicDetector('typescript.backend.controller-business-logic', [
|
|
923
|
+
'heuristics.ts.backend.controller-business-logic.ast',
|
|
924
|
+
]),
|
|
925
|
+
'skills.backend.guideline.backend.no-lo-gica-de-negocio-en-repositorios-solo-crud-y-queries':
|
|
926
|
+
heuristicDetector('typescript.backend.repository-business-logic', [
|
|
927
|
+
'heuristics.ts.backend.repository-business-logic.ast',
|
|
928
|
+
]),
|
|
929
|
+
'skills.backend.guideline.backend.repositories-para-datos-abstraer-acceso-a-bd-con-interfaces':
|
|
930
|
+
heuristicDetector('typescript.backend.repository-business-logic', [
|
|
931
|
+
'heuristics.ts.backend.repository-business-logic.ast',
|
|
932
|
+
]),
|
|
901
933
|
'skills.backend.guideline.backend.anemic-domain-models-entidades-solo-con-getters-setters':
|
|
902
934
|
heuristicDetector('typescript.backend.anemic-domain-model', [
|
|
903
935
|
'heuristics.ts.backend.anemic-domain-model.ast',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pumuki",
|
|
3
|
-
"version": "6.3.
|
|
3
|
+
"version": "6.3.294",
|
|
4
4
|
"description": "Enterprise-grade AST Intelligence System with multi-platform support (iOS, Android, Backend, Frontend) and Feature-First + DDD + Clean Architecture enforcement. Includes dynamic violations API for intelligent querying.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|