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
|
@@ -76,6 +76,17 @@ const isBackendTypeScriptPath = (path: string): boolean => {
|
|
|
76
76
|
return isTypeScriptHeuristicTargetPath(path) && path.startsWith('apps/backend/');
|
|
77
77
|
};
|
|
78
78
|
|
|
79
|
+
const isBackendNonConfigTypeScriptPath = (path: string): boolean => {
|
|
80
|
+
const normalized = path.replace(/\\/g, '/').toLowerCase();
|
|
81
|
+
return (
|
|
82
|
+
isBackendTypeScriptPath(path) &&
|
|
83
|
+
!normalized.includes('/config/') &&
|
|
84
|
+
!normalized.endsWith('.config.ts') &&
|
|
85
|
+
!normalized.endsWith('/configuration.ts') &&
|
|
86
|
+
!normalized.endsWith('/environment.ts')
|
|
87
|
+
);
|
|
88
|
+
};
|
|
89
|
+
|
|
79
90
|
const isBackendControllerTypeScriptPath = (path: string): boolean => {
|
|
80
91
|
const normalized = path.replace(/\\/g, '/').toLowerCase();
|
|
81
92
|
return isBackendTypeScriptPath(path) && normalized.endsWith('.controller.ts');
|
|
@@ -95,6 +106,10 @@ const isIOSSwiftPath = (path: string): boolean => {
|
|
|
95
106
|
return path.endsWith('.swift') && path.startsWith('apps/ios/');
|
|
96
107
|
};
|
|
97
108
|
|
|
109
|
+
const isIOSSwiftPackageManifestPath = (path: string): boolean => {
|
|
110
|
+
return path.replace(/\\/g, '/') === 'apps/ios/Package.swift';
|
|
111
|
+
};
|
|
112
|
+
|
|
98
113
|
const isIOSPodfilePath = (path: string): boolean => {
|
|
99
114
|
const normalized = path.replace(/\\/g, '/');
|
|
100
115
|
return (
|
|
@@ -491,6 +506,10 @@ const astDetectorRegistry: ReadonlyArray<ASTDetectorRegistryEntry> = [
|
|
|
491
506
|
{ detect: TS.hasConsoleErrorCall, ruleId: 'heuristics.ts.console-error.ast', code: 'HEURISTICS_CONSOLE_ERROR_AST', message: 'AST heuristic detected console.error usage.' },
|
|
492
507
|
{ detect: TS.hasSensitiveDataLoggingCall, ruleId: 'heuristics.ts.backend.sensitive-data-logging.ast', code: 'HEURISTICS_BACKEND_SENSITIVE_DATA_LOGGING_AST', message: 'AST heuristic detected sensitive data passed to a logging sink.' },
|
|
493
508
|
{ detect: TS.hasBackendLogWithoutContext, locateLines: TS.findBackendLogWithoutContextLines, ruleId: 'heuristics.ts.backend.log-without-context.ast', code: 'HEURISTICS_BACKEND_LOG_WITHOUT_CONTEXT_AST', message: 'AST heuristic detected backend log call without request/user/trace context.', pathCheck: isBackendTypeScriptPath },
|
|
509
|
+
{ detect: TS.hasBackendAuthResponseWithoutRefreshToken, locateLines: TS.findBackendAuthResponseWithoutRefreshTokenLines, ruleId: 'heuristics.ts.backend.auth-response-without-refresh-token.ast', code: 'HEURISTICS_BACKEND_AUTH_RESPONSE_WITHOUT_REFRESH_TOKEN_AST', message: 'AST heuristic detected backend auth response returning an access token without a refresh token.', pathCheck: isBackendTypeScriptPath },
|
|
510
|
+
{ detect: TS.hasBackendProcessEnvDefaultFallback, locateLines: TS.findBackendProcessEnvDefaultFallbackLines, ruleId: 'heuristics.ts.backend.process-env-default-fallback.ast', code: 'HEURISTICS_BACKEND_PROCESS_ENV_DEFAULT_FALLBACK_AST', message: 'AST heuristic detected backend process.env configuration with a literal production fallback.', pathCheck: isBackendTypeScriptPath },
|
|
511
|
+
{ detect: TS.hasBackendDirectProcessEnvRead, locateLines: TS.findBackendDirectProcessEnvReadLines, ruleId: 'heuristics.ts.backend.direct-process-env-read.ast', code: 'HEURISTICS_BACKEND_DIRECT_PROCESS_ENV_READ_AST', message: 'AST heuristic detected direct backend process.env read outside the configuration boundary.', pathCheck: isBackendNonConfigTypeScriptPath },
|
|
512
|
+
{ detect: TS.hasBackendConfigModuleWithoutValidation, locateLines: TS.findBackendConfigModuleWithoutValidationLines, ruleId: 'heuristics.ts.backend.config-module-without-validation.ast', code: 'HEURISTICS_BACKEND_CONFIG_MODULE_WITHOUT_VALIDATION_AST', message: 'AST heuristic detected ConfigModule.forRoot without env validationSchema or validate.', pathCheck: isBackendTypeScriptPath },
|
|
494
513
|
{ detect: TS.hasBackendMagicNumberLiteral, ruleId: 'heuristics.ts.backend.magic-number-literal.ast', code: 'HEURISTICS_BACKEND_MAGIC_NUMBER_LITERAL_AST', message: 'AST heuristic detected an inline numeric literal; use a named constant for backend readability and maintainability.', pathCheck: isBackendTypeScriptPath },
|
|
495
514
|
{ detect: TS.hasBackendGenericErrorThrow, ruleId: 'heuristics.ts.backend.generic-error-throw.ast', code: 'HEURISTICS_BACKEND_GENERIC_ERROR_THROW_AST', message: 'AST heuristic detected throw new Error in backend code; use a specific domain/application exception.', pathCheck: isBackendTypeScriptPath },
|
|
496
515
|
{ detect: TS.hasBackendRawThrowExpression, locateLines: TS.findBackendRawThrowExpressionLines, ruleId: 'heuristics.ts.backend.raw-throw-expression.ast', code: 'HEURISTICS_BACKEND_RAW_THROW_EXPRESSION_AST', message: 'AST heuristic detected raw throw expression in backend code; throw a typed exception.', pathCheck: isBackendTypeScriptPath },
|
|
@@ -499,9 +518,15 @@ const astDetectorRegistry: ReadonlyArray<ASTDetectorRegistryEntry> = [
|
|
|
499
518
|
{ 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
519
|
{ 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
520
|
{ 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 },
|
|
521
|
+
{ 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 },
|
|
522
|
+
{ 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
523
|
{ 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
524
|
{ 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 },
|
|
525
|
+
{ 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 },
|
|
526
|
+
{ detect: TS.hasBackendMissingCompressionMiddleware, locateLines: TS.findBackendMissingCompressionMiddlewareLines, ruleId: 'heuristics.ts.backend.missing-compression-middleware.ast', code: 'HEURISTICS_BACKEND_MISSING_COMPRESSION_MIDDLEWARE_AST', message: 'AST heuristic detected NestJS bootstrap without compression middleware.', pathCheck: isBackendTypeScriptPath },
|
|
527
|
+
{ 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
528
|
{ 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 },
|
|
529
|
+
{ detect: TS.hasBackendSensitiveCacheWrite, locateLines: TS.findBackendSensitiveCacheWriteLines, ruleId: 'heuristics.ts.backend.sensitive-cache-write.ast', code: 'HEURISTICS_BACKEND_SENSITIVE_CACHE_WRITE_AST', message: 'AST heuristic detected sensitive data written to backend cache.', pathCheck: isBackendTypeScriptPath },
|
|
505
530
|
{ detect: TS.hasEvalCall, ruleId: 'heuristics.ts.eval.ast', code: 'HEURISTICS_EVAL_AST', message: 'AST heuristic detected eval usage.' },
|
|
506
531
|
{ detect: TS.hasFunctionConstructorUsage, ruleId: 'heuristics.ts.function-constructor.ast', code: 'HEURISTICS_FUNCTION_CONSTRUCTOR_AST', message: 'AST heuristic detected Function constructor usage.' },
|
|
507
532
|
{ detect: TS.hasSetTimeoutStringCallback, ruleId: 'heuristics.ts.set-timeout-string.ast', code: 'HEURISTICS_SET_TIMEOUT_STRING_AST', message: 'AST heuristic detected setTimeout with a string callback.' },
|
|
@@ -527,9 +552,13 @@ const astDetectorRegistry: ReadonlyArray<ASTDetectorRegistryEntry> = [
|
|
|
527
552
|
{ 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
553
|
{ 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
554
|
{ 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 },
|
|
555
|
+
{ 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 },
|
|
556
|
+
{ detect: TS.hasBackendAuthRouteWithoutThrottle, locateLines: TS.findBackendAuthRouteWithoutThrottleLines, ruleId: 'heuristics.ts.backend.auth-route-without-throttle.ast', code: 'HEURISTICS_BACKEND_AUTH_ROUTE_WITHOUT_THROTTLE_AST', message: 'AST heuristic detected authentication route without NestJS throttling/rate limiting.', pathCheck: isBackendTypeScriptPath },
|
|
557
|
+
{ detect: TS.hasBackendEventHandlerWithoutOnEvent, locateLines: TS.findBackendEventHandlerWithoutOnEventLines, ruleId: 'heuristics.ts.backend.event-handler-without-on-event.ast', code: 'HEURISTICS_BACKEND_EVENT_HANDLER_WITHOUT_ON_EVENT_AST', message: 'AST heuristic detected backend event handler without @OnEvent subscription.', pathCheck: isBackendTypeScriptPath },
|
|
530
558
|
{ 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
559
|
{ 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
560
|
{ 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.' },
|
|
561
|
+
{ detect: TS.hasDtoPropertyWithoutApiProperty, locateLines: TS.findDtoPropertyWithoutApiPropertyLines, ruleId: 'heuristics.ts.backend.dto-property-without-api-property.ast', code: 'HEURISTICS_BACKEND_DTO_PROPERTY_WITHOUT_API_PROPERTY_AST', message: 'AST heuristic detected DTO property without Swagger ApiProperty decorator.' },
|
|
533
562
|
{ detect: TS.hasDtoNestedPropertyWithoutNestedValidation, ruleId: 'heuristics.ts.backend.dto-nested-property-without-nested-validation.ast', code: 'HEURISTICS_BACKEND_DTO_NESTED_PROPERTY_WITHOUT_NESTED_VALIDATION_AST', message: 'AST heuristic detected nested DTO property without @ValidateNested/@Type decorators.' },
|
|
534
563
|
{ detect: TS.hasLargeClassDeclaration, ruleId: 'heuristics.ts.god-class-large-class.ast', code: 'HEURISTICS_GOD_CLASS_LARGE_CLASS_AST', message: 'AST heuristic detected God Class candidate by mixed responsibility nodes in a single class declaration.' },
|
|
535
564
|
{ detect: TS.hasRecordStringUnknownType, locateLines: TS.findRecordStringUnknownTypeLines, ruleId: 'common.types.record_unknown_requires_type', code: 'COMMON_TYPES_RECORD_UNKNOWN_REQUIRES_TYPE_AST', message: 'AST heuristic detected Record<string, unknown> without explicit value union.' },
|
|
@@ -718,6 +747,7 @@ const textDetectorRegistry: ReadonlyArray<TextDetectorRegistryEntry> = [
|
|
|
718
747
|
// iOS
|
|
719
748
|
{ platform: 'ios', pathCheck: isIOSPodfilePath, excludePaths: [], detect: detectsTrackedFilePresence, ruleId: 'heuristics.ios.dependencies.cocoapods.ast', code: 'HEURISTICS_IOS_DEPENDENCIES_COCOAPODS_AST', message: 'AST heuristic detected CocoaPods dependency files in an iOS project; Swift Package Manager remains the preferred baseline for new code.' },
|
|
720
749
|
{ platform: 'ios', pathCheck: isIOSCartfilePath, excludePaths: [], detect: detectsTrackedFilePresence, ruleId: 'heuristics.ios.dependencies.carthage.ast', code: 'HEURISTICS_IOS_DEPENDENCIES_CARTHAGE_AST', message: 'AST heuristic detected Carthage dependency files in an iOS project; Swift Package Manager remains the preferred baseline for new code.' },
|
|
750
|
+
{ platform: 'ios', pathCheck: isIOSSwiftPackageManifestPath, excludePaths: [], detect: TextIOS.hasSwiftPackageBranchDependencyUsage, locateLines: TextIOS.collectSwiftPackageBranchDependencyLines, primaryNode: (lines) => ({ kind: 'call', name: 'SwiftPM .package(..., branch: ...)', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: .package(..., exact:/from: version)', lines }], why: 'Branch-based SwiftPM dependencies drift over time and do not provide a reproducible iOS dependency graph.', impact: 'A consumer can build different code from the same commit when the remote branch moves, making production audits and regressions non-deterministic.', expected_fix: 'Pin the dependency to an exact version or an approved semantic version requirement in Package.swift; avoid branch-based dependencies outside explicitly approved experiments.', ruleId: 'heuristics.ios.dependencies.swiftpm-branch-dependency.ast', code: 'HEURISTICS_IOS_DEPENDENCIES_SWIFTPM_BRANCH_DEPENDENCY_AST', message: 'AST heuristic detected a branch-based SwiftPM dependency in iOS Package.swift; use specific versions for reproducible builds.' },
|
|
721
751
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftForceUnwrap, locateLines: TextIOS.collectSwiftForceUnwrapLines, primaryNode: (lines) => ({ kind: 'member', name: 'force unwrap postfix !', lines }), relatedNodes: (lines) => [{ kind: 'member', name: 'replacement: guarded optional binding or explicit failure path', lines }], why: 'Force unwrap turns optional handling into a runtime crash path instead of a checked domain, UI or infrastructure decision.', impact: 'A nil value can terminate the app outside the error boundary, making production behavior non-deterministic and hard to recover or test.', expected_fix: 'Replace postfix ! with guard let, if let, nil coalescing, throwing validation, or an explicit fallback. In modern Swift tests prefer #require when the unwrap is part of an assertion contract.', ruleId: 'heuristics.ios.force-unwrap.ast', code: 'HEURISTICS_IOS_FORCE_UNWRAP_AST', message: 'AST heuristic detected force unwrap usage.' },
|
|
722
752
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftAnyViewUsage, locateLines: TextIOS.collectSwiftAnyViewLines, primaryNode: (lines) => ({ kind: 'call', name: 'type erasure wrapper AnyView', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: concrete View composition or @ViewBuilder branch', lines }], why: 'AnyView erases SwiftUI view identity and type information, hiding structural changes from the compiler and making diffing less predictable.', impact: 'SwiftUI may lose optimization opportunities, navigation/sheet branches become harder to reason about, and remediating UI regressions requires reading dynamic wrappers instead of concrete view composition.', expected_fix: 'Replace AnyView with concrete some View composition, @ViewBuilder branching, generic View parameters, or small extracted subviews that preserve static SwiftUI identity.', ruleId: 'heuristics.ios.anyview.ast', code: 'HEURISTICS_IOS_ANYVIEW_AST', message: 'AST heuristic detected AnyView usage.' },
|
|
723
753
|
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftAnyTypeErasureUsage, locateLines: TextIOS.collectSwiftAnyTypeErasureLines, primaryNode: (lines) => ({ kind: 'property', name: 'Swift Any/AnyObject/AnyHashable type erasure', lines }), relatedNodes: (lines) => [{ kind: 'member', name: 'replacement: generics, associated types, protocol boundary or concrete domain type', lines }], why: 'General Swift type erasure hides domain contracts that should be expressed with generics, associated types or explicit protocol boundaries.', impact: 'Callers lose compile-time guarantees, invalid states travel through the codebase and remediation becomes runtime/debug driven instead of type-system driven.', expected_fix: 'Replace Any, AnyObject or AnyHashable usage with a generic parameter, associated type, concrete value object or narrow protocol boundary.', ruleId: 'heuristics.ios.type-erasure.any.ast', code: 'HEURISTICS_IOS_TYPE_ERASURE_ANY_AST', message: 'AST heuristic detected Swift Any/AnyObject/AnyHashable type erasure in production code.' },
|
|
@@ -733,8 +763,9 @@ const textDetectorRegistry: ReadonlyArray<TextDetectorRegistryEntry> = [
|
|
|
733
763
|
{ 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
764
|
{ 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
765
|
{ 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:
|
|
737
|
-
{ platform: 'ios', pathCheck: isIOSPresentationPath, excludePaths: [isSwiftTestPath], detect: TextIOS.
|
|
766
|
+
{ platform: 'ios', pathCheck: isIOSSwiftPath, excludePaths: [isSwiftTestPath], detect: TextIOS.hasSwiftNSErrorThrowUsage, locateLines: TextIOS.collectSwiftNSErrorThrowLines, primaryNode: (lines) => ({ kind: 'call', name: 'throw NSError(...)', lines }), relatedNodes: (lines) => [{ kind: 'call', name: 'replacement: typed Swift Error enum case', lines }], why: 'NSError throws bypass the typed Swift error boundary that should model network, domain or infrastructure failures explicitly.', impact: 'Callers receive an untyped Foundation error instead of a remediable enum case, making recovery, tests and user-facing handling less deterministic.', expected_fix: 'Define a domain-specific Error enum such as NetworkError or AppError and throw typed cases instead of constructing NSError directly.', ruleId: 'heuristics.ios.error.nserror-throw.ast', code: 'HEURISTICS_IOS_ERROR_NSERROR_THROW_AST', message: 'AST heuristic detected throw NSError(...) in iOS production code; use typed Swift Error enums such as NetworkError or AppError.' },
|
|
767
|
+
{ 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.' },
|
|
768
|
+
{ 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
769
|
{ 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
770
|
{ 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
771
|
{ 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 { iosRules } from './ios';
|
|
4
4
|
|
|
5
5
|
test('iosRules define reglas heurísticas locked para plataforma ios', () => {
|
|
6
|
-
assert.equal(iosRules.length,
|
|
6
|
+
assert.equal(iosRules.length, 105);
|
|
7
7
|
|
|
8
8
|
const ids = iosRules.map((rule) => rule.id);
|
|
9
9
|
assert.deepEqual(ids, [
|
|
@@ -21,6 +21,7 @@ test('iosRules define reglas heurísticas locked para plataforma ios', () => {
|
|
|
21
21
|
'heuristics.ios.task-detached.ast',
|
|
22
22
|
'heuristics.ios.concurrency.async-without-await.ast',
|
|
23
23
|
'heuristics.ios.error.empty-catch.ast',
|
|
24
|
+
'heuristics.ios.error.nserror-throw.ast',
|
|
24
25
|
'heuristics.ios.swiftui.onappear-task.ast',
|
|
25
26
|
'heuristics.ios.swiftui.onchange-task.ast',
|
|
26
27
|
'heuristics.ios.swiftui.onchange-readonly-var.ast',
|
|
@@ -43,6 +44,7 @@ test('iosRules define reglas heurísticas locked para plataforma ios', () => {
|
|
|
43
44
|
'heuristics.ios.json.jsonserialization.ast',
|
|
44
45
|
'heuristics.ios.dependencies.cocoapods.ast',
|
|
45
46
|
'heuristics.ios.dependencies.carthage.ast',
|
|
47
|
+
'heuristics.ios.dependencies.swiftpm-branch-dependency.ast',
|
|
46
48
|
'heuristics.ios.security.userdefaults-sensitive-data.ast',
|
|
47
49
|
'heuristics.ios.security.insecure-transport.ast',
|
|
48
50
|
'heuristics.ios.localization.localizable-strings.ast',
|
|
@@ -249,6 +251,14 @@ test('iosRules define reglas heurísticas locked para plataforma ios', () => {
|
|
|
249
251
|
byId.get('heuristics.ios.assume-isolated.ast')?.then.code,
|
|
250
252
|
'HEURISTICS_IOS_ASSUME_ISOLATED_AST'
|
|
251
253
|
);
|
|
254
|
+
assert.equal(
|
|
255
|
+
byId.get('heuristics.ios.dependencies.swiftpm-branch-dependency.ast')?.then.code,
|
|
256
|
+
'HEURISTICS_IOS_DEPENDENCIES_SWIFTPM_BRANCH_DEPENDENCY_AST'
|
|
257
|
+
);
|
|
258
|
+
assert.equal(
|
|
259
|
+
byId.get('heuristics.ios.error.nserror-throw.ast')?.then.code,
|
|
260
|
+
'HEURISTICS_IOS_ERROR_NSERROR_THROW_AST'
|
|
261
|
+
);
|
|
252
262
|
assert.equal(
|
|
253
263
|
byId.get('heuristics.ios.legacy-swiftui-observable-wrapper.ast')?.then.code,
|
|
254
264
|
'HEURISTICS_IOS_LEGACY_SWIFTUI_OBSERVABLE_WRAPPER_AST'
|
|
@@ -255,6 +255,24 @@ export const iosRules: RuleSet = [
|
|
|
255
255
|
code: 'HEURISTICS_IOS_ERROR_EMPTY_CATCH_AST',
|
|
256
256
|
},
|
|
257
257
|
},
|
|
258
|
+
{
|
|
259
|
+
id: 'heuristics.ios.error.nserror-throw.ast',
|
|
260
|
+
description: 'Detects direct NSError throws where typed Swift Error enums should be used.',
|
|
261
|
+
severity: 'WARN',
|
|
262
|
+
platform: 'ios',
|
|
263
|
+
locked: true,
|
|
264
|
+
when: {
|
|
265
|
+
kind: 'Heuristic',
|
|
266
|
+
where: {
|
|
267
|
+
ruleId: 'heuristics.ios.error.nserror-throw.ast',
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
then: {
|
|
271
|
+
kind: 'Finding',
|
|
272
|
+
message: 'AST heuristic detected throw NSError(...) in iOS production code.',
|
|
273
|
+
code: 'HEURISTICS_IOS_ERROR_NSERROR_THROW_AST',
|
|
274
|
+
},
|
|
275
|
+
},
|
|
258
276
|
{
|
|
259
277
|
id: 'heuristics.ios.swiftui.onappear-task.ast',
|
|
260
278
|
description: 'Detects Task launches from SwiftUI onAppear where .task can provide lifecycle cancellation.',
|
|
@@ -667,6 +685,24 @@ export const iosRules: RuleSet = [
|
|
|
667
685
|
code: 'HEURISTICS_IOS_DEPENDENCIES_CARTHAGE_AST',
|
|
668
686
|
},
|
|
669
687
|
},
|
|
688
|
+
{
|
|
689
|
+
id: 'heuristics.ios.dependencies.swiftpm-branch-dependency.ast',
|
|
690
|
+
description: 'Detects branch-based SwiftPM dependencies in iOS Package.swift.',
|
|
691
|
+
severity: 'WARN',
|
|
692
|
+
platform: 'ios',
|
|
693
|
+
locked: true,
|
|
694
|
+
when: {
|
|
695
|
+
kind: 'Heuristic',
|
|
696
|
+
where: {
|
|
697
|
+
ruleId: 'heuristics.ios.dependencies.swiftpm-branch-dependency.ast',
|
|
698
|
+
},
|
|
699
|
+
},
|
|
700
|
+
then: {
|
|
701
|
+
kind: 'Finding',
|
|
702
|
+
message: 'AST heuristic detected a branch-based SwiftPM dependency.',
|
|
703
|
+
code: 'HEURISTICS_IOS_DEPENDENCIES_SWIFTPM_BRANCH_DEPENDENCY_AST',
|
|
704
|
+
},
|
|
705
|
+
},
|
|
670
706
|
{
|
|
671
707
|
id: 'heuristics.ios.security.userdefaults-sensitive-data.ast',
|
|
672
708
|
description: 'Detects sensitive data stored in UserDefaults/AppStorage; Keychain is the preferred baseline for secrets.',
|
|
@@ -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, 61);
|
|
7
7
|
|
|
8
8
|
const ids = typescriptRules.map((rule) => rule.id);
|
|
9
9
|
assert.deepEqual(ids, [
|
|
@@ -49,10 +49,24 @@ 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',
|
|
53
|
+
'heuristics.ts.backend.auth-route-without-throttle.ast',
|
|
54
|
+
'heuristics.ts.backend.event-handler-without-on-event.ast',
|
|
52
55
|
'heuristics.ts.backend.persistence-mutation-without-audit-event.ast',
|
|
56
|
+
'heuristics.ts.backend.sensitive-cache-write.ast',
|
|
57
|
+
'heuristics.ts.backend.auth-response-without-refresh-token.ast',
|
|
58
|
+
'heuristics.ts.backend.process-env-default-fallback.ast',
|
|
59
|
+
'heuristics.ts.backend.direct-process-env-read.ast',
|
|
60
|
+
'heuristics.ts.backend.config-module-without-validation.ast',
|
|
53
61
|
'heuristics.ts.backend.hard-delete-without-soft-delete.ast',
|
|
54
62
|
'heuristics.ts.backend.dto-property-without-validation.ast',
|
|
63
|
+
'heuristics.ts.backend.dto-property-without-api-property.ast',
|
|
55
64
|
'heuristics.ts.backend.dto-nested-property-without-nested-validation.ast',
|
|
65
|
+
'heuristics.ts.backend.missing-global-validation-pipe.ast',
|
|
66
|
+
'heuristics.ts.backend.missing-helmet-security-headers.ast',
|
|
67
|
+
'heuristics.ts.backend.missing-compression-middleware.ast',
|
|
68
|
+
'heuristics.ts.backend.controller-business-logic.ast',
|
|
69
|
+
'heuristics.ts.backend.repository-business-logic.ast',
|
|
56
70
|
'heuristics.ts.god-class-large-class.ast',
|
|
57
71
|
]);
|
|
58
72
|
|
|
@@ -169,10 +183,42 @@ test('typescriptRules define reglas heurísticas locked para plataforma generic'
|
|
|
169
183
|
byId.get('heuristics.ts.backend.controller-route-without-guard.ast')?.then.code,
|
|
170
184
|
'HEURISTICS_BACKEND_CONTROLLER_ROUTE_WITHOUT_GUARD_AST'
|
|
171
185
|
);
|
|
186
|
+
assert.equal(
|
|
187
|
+
byId.get('heuristics.ts.backend.controller-route-without-roles.ast')?.then.code,
|
|
188
|
+
'HEURISTICS_BACKEND_CONTROLLER_ROUTE_WITHOUT_ROLES_AST'
|
|
189
|
+
);
|
|
190
|
+
assert.equal(
|
|
191
|
+
byId.get('heuristics.ts.backend.auth-route-without-throttle.ast')?.then.code,
|
|
192
|
+
'HEURISTICS_BACKEND_AUTH_ROUTE_WITHOUT_THROTTLE_AST'
|
|
193
|
+
);
|
|
194
|
+
assert.equal(
|
|
195
|
+
byId.get('heuristics.ts.backend.event-handler-without-on-event.ast')?.then.code,
|
|
196
|
+
'HEURISTICS_BACKEND_EVENT_HANDLER_WITHOUT_ON_EVENT_AST'
|
|
197
|
+
);
|
|
172
198
|
assert.equal(
|
|
173
199
|
byId.get('heuristics.ts.backend.persistence-mutation-without-audit-event.ast')?.then.code,
|
|
174
200
|
'HEURISTICS_BACKEND_PERSISTENCE_MUTATION_WITHOUT_AUDIT_EVENT_AST'
|
|
175
201
|
);
|
|
202
|
+
assert.equal(
|
|
203
|
+
byId.get('heuristics.ts.backend.sensitive-cache-write.ast')?.then.code,
|
|
204
|
+
'HEURISTICS_BACKEND_SENSITIVE_CACHE_WRITE_AST'
|
|
205
|
+
);
|
|
206
|
+
assert.equal(
|
|
207
|
+
byId.get('heuristics.ts.backend.auth-response-without-refresh-token.ast')?.then.code,
|
|
208
|
+
'HEURISTICS_BACKEND_AUTH_RESPONSE_WITHOUT_REFRESH_TOKEN_AST'
|
|
209
|
+
);
|
|
210
|
+
assert.equal(
|
|
211
|
+
byId.get('heuristics.ts.backend.process-env-default-fallback.ast')?.then.code,
|
|
212
|
+
'HEURISTICS_BACKEND_PROCESS_ENV_DEFAULT_FALLBACK_AST'
|
|
213
|
+
);
|
|
214
|
+
assert.equal(
|
|
215
|
+
byId.get('heuristics.ts.backend.direct-process-env-read.ast')?.then.code,
|
|
216
|
+
'HEURISTICS_BACKEND_DIRECT_PROCESS_ENV_READ_AST'
|
|
217
|
+
);
|
|
218
|
+
assert.equal(
|
|
219
|
+
byId.get('heuristics.ts.backend.config-module-without-validation.ast')?.then.code,
|
|
220
|
+
'HEURISTICS_BACKEND_CONFIG_MODULE_WITHOUT_VALIDATION_AST'
|
|
221
|
+
);
|
|
176
222
|
assert.equal(
|
|
177
223
|
byId.get('heuristics.ts.backend.hard-delete-without-soft-delete.ast')?.then.code,
|
|
178
224
|
'HEURISTICS_BACKEND_HARD_DELETE_WITHOUT_SOFT_DELETE_AST'
|
|
@@ -181,10 +227,22 @@ test('typescriptRules define reglas heurísticas locked para plataforma generic'
|
|
|
181
227
|
byId.get('heuristics.ts.backend.dto-property-without-validation.ast')?.then.code,
|
|
182
228
|
'HEURISTICS_BACKEND_DTO_PROPERTY_WITHOUT_VALIDATION_AST'
|
|
183
229
|
);
|
|
230
|
+
assert.equal(
|
|
231
|
+
byId.get('heuristics.ts.backend.dto-property-without-api-property.ast')?.then.code,
|
|
232
|
+
'HEURISTICS_BACKEND_DTO_PROPERTY_WITHOUT_API_PROPERTY_AST'
|
|
233
|
+
);
|
|
184
234
|
assert.equal(
|
|
185
235
|
byId.get('heuristics.ts.backend.dto-nested-property-without-nested-validation.ast')?.then.code,
|
|
186
236
|
'HEURISTICS_BACKEND_DTO_NESTED_PROPERTY_WITHOUT_NESTED_VALIDATION_AST'
|
|
187
237
|
);
|
|
238
|
+
assert.equal(
|
|
239
|
+
byId.get('heuristics.ts.backend.missing-helmet-security-headers.ast')?.then.code,
|
|
240
|
+
'HEURISTICS_BACKEND_MISSING_HELMET_SECURITY_HEADERS_AST'
|
|
241
|
+
);
|
|
242
|
+
assert.equal(
|
|
243
|
+
byId.get('heuristics.ts.backend.missing-compression-middleware.ast')?.then.code,
|
|
244
|
+
'HEURISTICS_BACKEND_MISSING_COMPRESSION_MIDDLEWARE_AST'
|
|
245
|
+
);
|
|
188
246
|
assert.equal(
|
|
189
247
|
byId.get('heuristics.ts.god-class-large-class.ast')?.then.code,
|
|
190
248
|
'HEURISTICS_GOD_CLASS_LARGE_CLASS_AST'
|
|
@@ -195,9 +253,18 @@ test('typescriptRules define reglas heurísticas locked para plataforma generic'
|
|
|
195
253
|
if (
|
|
196
254
|
rule.id === 'heuristics.ts.god-class-large-class.ast' ||
|
|
197
255
|
rule.id === 'heuristics.ts.backend.controller-route-without-guard.ast' ||
|
|
256
|
+
rule.id === 'heuristics.ts.backend.controller-route-without-roles.ast' ||
|
|
257
|
+
rule.id === 'heuristics.ts.backend.auth-route-without-throttle.ast' ||
|
|
258
|
+
rule.id === 'heuristics.ts.backend.event-handler-without-on-event.ast' ||
|
|
198
259
|
rule.id === 'heuristics.ts.backend.persistence-mutation-without-audit-event.ast' ||
|
|
260
|
+
rule.id === 'heuristics.ts.backend.sensitive-cache-write.ast' ||
|
|
261
|
+
rule.id === 'heuristics.ts.backend.auth-response-without-refresh-token.ast' ||
|
|
262
|
+
rule.id === 'heuristics.ts.backend.process-env-default-fallback.ast' ||
|
|
263
|
+
rule.id === 'heuristics.ts.backend.direct-process-env-read.ast' ||
|
|
264
|
+
rule.id === 'heuristics.ts.backend.config-module-without-validation.ast' ||
|
|
199
265
|
rule.id === 'heuristics.ts.backend.hard-delete-without-soft-delete.ast' ||
|
|
200
266
|
rule.id === 'heuristics.ts.backend.dto-property-without-validation.ast' ||
|
|
267
|
+
rule.id === 'heuristics.ts.backend.dto-property-without-api-property.ast' ||
|
|
201
268
|
rule.id === 'heuristics.ts.backend.dto-nested-property-without-nested-validation.ast' ||
|
|
202
269
|
rule.id === 'heuristics.ts.backend.sensitive-data-logging.ast' ||
|
|
203
270
|
rule.id === 'heuristics.ts.backend.log-without-context.ast' ||
|
|
@@ -211,6 +278,11 @@ test('typescriptRules define reglas heurísticas locked para plataforma generic'
|
|
|
211
278
|
rule.id === 'heuristics.ts.backend.controller-entity-response.ast' ||
|
|
212
279
|
rule.id === 'heuristics.ts.backend.anemic-domain-model.ast' ||
|
|
213
280
|
rule.id === 'heuristics.ts.backend.permissive-cors.ast' ||
|
|
281
|
+
rule.id === 'heuristics.ts.backend.missing-global-validation-pipe.ast' ||
|
|
282
|
+
rule.id === 'heuristics.ts.backend.missing-compression-middleware.ast' ||
|
|
283
|
+
rule.id === 'heuristics.ts.backend.missing-helmet-security-headers.ast' ||
|
|
284
|
+
rule.id === 'heuristics.ts.backend.controller-business-logic.ast' ||
|
|
285
|
+
rule.id === 'heuristics.ts.backend.repository-business-logic.ast' ||
|
|
214
286
|
rule.id === 'heuristics.ts.backend.string-literal-union-enum.ast' ||
|
|
215
287
|
rule.id === 'heuristics.ts.sensitive-token-in-url.ast'
|
|
216
288
|
) {
|
|
@@ -764,6 +764,62 @@ 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
|
+
},
|
|
786
|
+
{
|
|
787
|
+
id: 'heuristics.ts.backend.auth-route-without-throttle.ast',
|
|
788
|
+
description:
|
|
789
|
+
'Detects NestJS authentication routes such as login, register, refresh or password reset without @Throttle rate limiting.',
|
|
790
|
+
severity: 'ERROR',
|
|
791
|
+
platform: 'generic',
|
|
792
|
+
locked: true,
|
|
793
|
+
when: {
|
|
794
|
+
kind: 'Heuristic',
|
|
795
|
+
where: {
|
|
796
|
+
ruleId: 'heuristics.ts.backend.auth-route-without-throttle.ast',
|
|
797
|
+
},
|
|
798
|
+
},
|
|
799
|
+
then: {
|
|
800
|
+
kind: 'Finding',
|
|
801
|
+
message: 'AST heuristic detected authentication route without rate limiting.',
|
|
802
|
+
code: 'HEURISTICS_BACKEND_AUTH_ROUTE_WITHOUT_THROTTLE_AST',
|
|
803
|
+
},
|
|
804
|
+
},
|
|
805
|
+
{
|
|
806
|
+
id: 'heuristics.ts.backend.event-handler-without-on-event.ast',
|
|
807
|
+
description: 'Detects backend event handler classes or methods without an explicit @OnEvent subscription.',
|
|
808
|
+
severity: 'ERROR',
|
|
809
|
+
platform: 'generic',
|
|
810
|
+
locked: true,
|
|
811
|
+
when: {
|
|
812
|
+
kind: 'Heuristic',
|
|
813
|
+
where: {
|
|
814
|
+
ruleId: 'heuristics.ts.backend.event-handler-without-on-event.ast',
|
|
815
|
+
},
|
|
816
|
+
},
|
|
817
|
+
then: {
|
|
818
|
+
kind: 'Finding',
|
|
819
|
+
message: 'AST heuristic detected backend event handler without @OnEvent.',
|
|
820
|
+
code: 'HEURISTICS_BACKEND_EVENT_HANDLER_WITHOUT_ON_EVENT_AST',
|
|
821
|
+
},
|
|
822
|
+
},
|
|
767
823
|
{
|
|
768
824
|
id: 'heuristics.ts.backend.persistence-mutation-without-audit-event.ast',
|
|
769
825
|
description:
|
|
@@ -784,6 +840,96 @@ export const typescriptRules: RuleSet = [
|
|
|
784
840
|
code: 'HEURISTICS_BACKEND_PERSISTENCE_MUTATION_WITHOUT_AUDIT_EVENT_AST',
|
|
785
841
|
},
|
|
786
842
|
},
|
|
843
|
+
{
|
|
844
|
+
id: 'heuristics.ts.backend.sensitive-cache-write.ast',
|
|
845
|
+
description: 'Detects backend cache writes that store tokens, passwords, secrets, email or other sensitive data.',
|
|
846
|
+
severity: 'ERROR',
|
|
847
|
+
platform: 'generic',
|
|
848
|
+
locked: true,
|
|
849
|
+
when: {
|
|
850
|
+
kind: 'Heuristic',
|
|
851
|
+
where: {
|
|
852
|
+
ruleId: 'heuristics.ts.backend.sensitive-cache-write.ast',
|
|
853
|
+
},
|
|
854
|
+
},
|
|
855
|
+
then: {
|
|
856
|
+
kind: 'Finding',
|
|
857
|
+
message: 'AST heuristic detected sensitive data written to backend cache.',
|
|
858
|
+
code: 'HEURISTICS_BACKEND_SENSITIVE_CACHE_WRITE_AST',
|
|
859
|
+
},
|
|
860
|
+
},
|
|
861
|
+
{
|
|
862
|
+
id: 'heuristics.ts.backend.auth-response-without-refresh-token.ast',
|
|
863
|
+
description: 'Detects backend auth responses that return an access token without a refresh token.',
|
|
864
|
+
severity: 'ERROR',
|
|
865
|
+
platform: 'generic',
|
|
866
|
+
locked: true,
|
|
867
|
+
when: {
|
|
868
|
+
kind: 'Heuristic',
|
|
869
|
+
where: {
|
|
870
|
+
ruleId: 'heuristics.ts.backend.auth-response-without-refresh-token.ast',
|
|
871
|
+
},
|
|
872
|
+
},
|
|
873
|
+
then: {
|
|
874
|
+
kind: 'Finding',
|
|
875
|
+
message: 'AST heuristic detected auth response without refresh token.',
|
|
876
|
+
code: 'HEURISTICS_BACKEND_AUTH_RESPONSE_WITHOUT_REFRESH_TOKEN_AST',
|
|
877
|
+
},
|
|
878
|
+
},
|
|
879
|
+
{
|
|
880
|
+
id: 'heuristics.ts.backend.process-env-default-fallback.ast',
|
|
881
|
+
description: 'Detects backend process.env configuration reads with literal production fallbacks.',
|
|
882
|
+
severity: 'ERROR',
|
|
883
|
+
platform: 'generic',
|
|
884
|
+
locked: true,
|
|
885
|
+
when: {
|
|
886
|
+
kind: 'Heuristic',
|
|
887
|
+
where: {
|
|
888
|
+
ruleId: 'heuristics.ts.backend.process-env-default-fallback.ast',
|
|
889
|
+
},
|
|
890
|
+
},
|
|
891
|
+
then: {
|
|
892
|
+
kind: 'Finding',
|
|
893
|
+
message: 'AST heuristic detected process.env fallback; fail fast when critical backend configuration is missing.',
|
|
894
|
+
code: 'HEURISTICS_BACKEND_PROCESS_ENV_DEFAULT_FALLBACK_AST',
|
|
895
|
+
},
|
|
896
|
+
},
|
|
897
|
+
{
|
|
898
|
+
id: 'heuristics.ts.backend.direct-process-env-read.ast',
|
|
899
|
+
description: 'Detects direct backend process.env reads outside the configuration boundary.',
|
|
900
|
+
severity: 'ERROR',
|
|
901
|
+
platform: 'generic',
|
|
902
|
+
locked: true,
|
|
903
|
+
when: {
|
|
904
|
+
kind: 'Heuristic',
|
|
905
|
+
where: {
|
|
906
|
+
ruleId: 'heuristics.ts.backend.direct-process-env-read.ast',
|
|
907
|
+
},
|
|
908
|
+
},
|
|
909
|
+
then: {
|
|
910
|
+
kind: 'Finding',
|
|
911
|
+
message: 'AST heuristic detected direct process.env access; use NestJS ConfigModule/ConfigService.',
|
|
912
|
+
code: 'HEURISTICS_BACKEND_DIRECT_PROCESS_ENV_READ_AST',
|
|
913
|
+
},
|
|
914
|
+
},
|
|
915
|
+
{
|
|
916
|
+
id: 'heuristics.ts.backend.config-module-without-validation.ast',
|
|
917
|
+
description: 'Detects NestJS ConfigModule.forRoot calls without environment validation.',
|
|
918
|
+
severity: 'ERROR',
|
|
919
|
+
platform: 'generic',
|
|
920
|
+
locked: true,
|
|
921
|
+
when: {
|
|
922
|
+
kind: 'Heuristic',
|
|
923
|
+
where: {
|
|
924
|
+
ruleId: 'heuristics.ts.backend.config-module-without-validation.ast',
|
|
925
|
+
},
|
|
926
|
+
},
|
|
927
|
+
then: {
|
|
928
|
+
kind: 'Finding',
|
|
929
|
+
message: 'AST heuristic detected ConfigModule.forRoot without validationSchema or validate.',
|
|
930
|
+
code: 'HEURISTICS_BACKEND_CONFIG_MODULE_WITHOUT_VALIDATION_AST',
|
|
931
|
+
},
|
|
932
|
+
},
|
|
787
933
|
{
|
|
788
934
|
id: 'heuristics.ts.backend.hard-delete-without-soft-delete.ast',
|
|
789
935
|
description: 'Detects backend physical delete calls where soft delete with deletedAt/deleted_at should be used.',
|
|
@@ -822,6 +968,25 @@ export const typescriptRules: RuleSet = [
|
|
|
822
968
|
code: 'HEURISTICS_BACKEND_DTO_PROPERTY_WITHOUT_VALIDATION_AST',
|
|
823
969
|
},
|
|
824
970
|
},
|
|
971
|
+
{
|
|
972
|
+
id: 'heuristics.ts.backend.dto-property-without-api-property.ast',
|
|
973
|
+
description:
|
|
974
|
+
'Detects DTO properties without Swagger ApiProperty or ApiPropertyOptional decorators.',
|
|
975
|
+
severity: 'ERROR',
|
|
976
|
+
platform: 'generic',
|
|
977
|
+
locked: true,
|
|
978
|
+
when: {
|
|
979
|
+
kind: 'Heuristic',
|
|
980
|
+
where: {
|
|
981
|
+
ruleId: 'heuristics.ts.backend.dto-property-without-api-property.ast',
|
|
982
|
+
},
|
|
983
|
+
},
|
|
984
|
+
then: {
|
|
985
|
+
kind: 'Finding',
|
|
986
|
+
message: 'AST heuristic detected DTO property without Swagger ApiProperty decorator.',
|
|
987
|
+
code: 'HEURISTICS_BACKEND_DTO_PROPERTY_WITHOUT_API_PROPERTY_AST',
|
|
988
|
+
},
|
|
989
|
+
},
|
|
825
990
|
{
|
|
826
991
|
id: 'heuristics.ts.backend.dto-nested-property-without-nested-validation.ast',
|
|
827
992
|
description: 'Detects nested DTO properties without ValidateNested/Type decorators.',
|
|
@@ -840,6 +1005,105 @@ export const typescriptRules: RuleSet = [
|
|
|
840
1005
|
code: 'HEURISTICS_BACKEND_DTO_NESTED_PROPERTY_WITHOUT_NESTED_VALIDATION_AST',
|
|
841
1006
|
},
|
|
842
1007
|
},
|
|
1008
|
+
{
|
|
1009
|
+
id: 'heuristics.ts.backend.missing-global-validation-pipe.ast',
|
|
1010
|
+
description:
|
|
1011
|
+
'Detects NestJS bootstrap code without a strict global ValidationPipe configured with whitelist=true.',
|
|
1012
|
+
severity: 'ERROR',
|
|
1013
|
+
platform: 'generic',
|
|
1014
|
+
locked: true,
|
|
1015
|
+
when: {
|
|
1016
|
+
kind: 'Heuristic',
|
|
1017
|
+
where: {
|
|
1018
|
+
ruleId: 'heuristics.ts.backend.missing-global-validation-pipe.ast',
|
|
1019
|
+
},
|
|
1020
|
+
},
|
|
1021
|
+
then: {
|
|
1022
|
+
kind: 'Finding',
|
|
1023
|
+
message:
|
|
1024
|
+
'AST heuristic detected NestJS bootstrap without global ValidationPipe whitelist.',
|
|
1025
|
+
code: 'HEURISTICS_BACKEND_MISSING_GLOBAL_VALIDATION_PIPE_AST',
|
|
1026
|
+
},
|
|
1027
|
+
},
|
|
1028
|
+
{
|
|
1029
|
+
id: 'heuristics.ts.backend.missing-helmet-security-headers.ast',
|
|
1030
|
+
description:
|
|
1031
|
+
'Detects NestJS bootstrap code without Helmet middleware for security headers.',
|
|
1032
|
+
severity: 'ERROR',
|
|
1033
|
+
platform: 'generic',
|
|
1034
|
+
locked: true,
|
|
1035
|
+
when: {
|
|
1036
|
+
kind: 'Heuristic',
|
|
1037
|
+
where: {
|
|
1038
|
+
ruleId: 'heuristics.ts.backend.missing-helmet-security-headers.ast',
|
|
1039
|
+
},
|
|
1040
|
+
},
|
|
1041
|
+
then: {
|
|
1042
|
+
kind: 'Finding',
|
|
1043
|
+
message:
|
|
1044
|
+
'AST heuristic detected NestJS bootstrap without Helmet security headers middleware.',
|
|
1045
|
+
code: 'HEURISTICS_BACKEND_MISSING_HELMET_SECURITY_HEADERS_AST',
|
|
1046
|
+
},
|
|
1047
|
+
},
|
|
1048
|
+
{
|
|
1049
|
+
id: 'heuristics.ts.backend.missing-compression-middleware.ast',
|
|
1050
|
+
description:
|
|
1051
|
+
'Detects NestJS bootstrap code without compression middleware for large responses.',
|
|
1052
|
+
severity: 'ERROR',
|
|
1053
|
+
platform: 'generic',
|
|
1054
|
+
locked: true,
|
|
1055
|
+
when: {
|
|
1056
|
+
kind: 'Heuristic',
|
|
1057
|
+
where: {
|
|
1058
|
+
ruleId: 'heuristics.ts.backend.missing-compression-middleware.ast',
|
|
1059
|
+
},
|
|
1060
|
+
},
|
|
1061
|
+
then: {
|
|
1062
|
+
kind: 'Finding',
|
|
1063
|
+
message: 'AST heuristic detected NestJS bootstrap without compression middleware.',
|
|
1064
|
+
code: 'HEURISTICS_BACKEND_MISSING_COMPRESSION_MIDDLEWARE_AST',
|
|
1065
|
+
},
|
|
1066
|
+
},
|
|
1067
|
+
{
|
|
1068
|
+
id: 'heuristics.ts.backend.controller-business-logic.ast',
|
|
1069
|
+
description:
|
|
1070
|
+
'Detects NestJS controller route handlers that contain business logic or direct persistence access instead of delegating to services/use cases.',
|
|
1071
|
+
severity: 'ERROR',
|
|
1072
|
+
platform: 'generic',
|
|
1073
|
+
locked: true,
|
|
1074
|
+
when: {
|
|
1075
|
+
kind: 'Heuristic',
|
|
1076
|
+
where: {
|
|
1077
|
+
ruleId: 'heuristics.ts.backend.controller-business-logic.ast',
|
|
1078
|
+
},
|
|
1079
|
+
},
|
|
1080
|
+
then: {
|
|
1081
|
+
kind: 'Finding',
|
|
1082
|
+
message:
|
|
1083
|
+
'AST heuristic detected business logic inside a NestJS controller route handler.',
|
|
1084
|
+
code: 'HEURISTICS_BACKEND_CONTROLLER_BUSINESS_LOGIC_AST',
|
|
1085
|
+
},
|
|
1086
|
+
},
|
|
1087
|
+
{
|
|
1088
|
+
id: 'heuristics.ts.backend.repository-business-logic.ast',
|
|
1089
|
+
description:
|
|
1090
|
+
'Detects backend repository methods that contain business decision branches instead of staying limited to CRUD, queries and data mapping.',
|
|
1091
|
+
severity: 'ERROR',
|
|
1092
|
+
platform: 'generic',
|
|
1093
|
+
locked: true,
|
|
1094
|
+
when: {
|
|
1095
|
+
kind: 'Heuristic',
|
|
1096
|
+
where: {
|
|
1097
|
+
ruleId: 'heuristics.ts.backend.repository-business-logic.ast',
|
|
1098
|
+
},
|
|
1099
|
+
},
|
|
1100
|
+
then: {
|
|
1101
|
+
kind: 'Finding',
|
|
1102
|
+
message:
|
|
1103
|
+
'AST heuristic detected business decision logic inside a backend repository.',
|
|
1104
|
+
code: 'HEURISTICS_BACKEND_REPOSITORY_BUSINESS_LOGIC_AST',
|
|
1105
|
+
},
|
|
1106
|
+
},
|
|
843
1107
|
{
|
|
844
1108
|
id: 'heuristics.ts.god-class-large-class.ast',
|
|
845
1109
|
description: 'Detects God Class candidates when one class mixes multiple responsibility nodes.',
|