feat: add entity exposure detection (v0.3.0)

Implement entity exposure detection to prevent domain entities
from leaking to API responses. Detects when controllers/routes
return domain entities instead of DTOs.

Features:
- EntityExposure value object with detailed suggestions
- IEntityExposureDetector interface in domain layer
- EntityExposureDetector implementation in infrastructure
- Integration into AnalyzeProject use case
- CLI display with helpful suggestions
- 24 comprehensive unit tests (98% coverage)
- Examples for bad and good patterns

Detection scope:
- Infrastructure layer only (controllers, routes, handlers, resolvers, gateways)
- Identifies PascalCase entities without Dto/Request/Response suffixes
- Parses async methods with Promise<T> return types
- Provides step-by-step remediation suggestions

Test coverage:
- EntityExposureDetector: 98.07%
- Overall project: 90.6% statements, 83.97% branches
- 218 tests passing

BREAKING CHANGE: Version bump to 0.3.0
This commit is contained in:
imfozilbek
2025-11-24 13:51:12 +05:00
parent a3cd71070e
commit f46048172f
14 changed files with 893 additions and 17 deletions

View File

@@ -0,0 +1,33 @@
// ❌ BAD: Exposing domain entity Order in API response
class Order {
constructor(
public id: string,
public items: OrderItem[],
public total: number,
public customerId: string,
) {}
}
class OrderItem {
constructor(
public productId: string,
public quantity: number,
public price: number,
) {}
}
class BadOrderController {
async getOrder(orderId: string): Promise<Order> {
return {
id: orderId,
items: [],
total: 100,
customerId: "customer-123",
}
}
async listOrders(): Promise<Order[]> {
return []
}
}

View File

@@ -0,0 +1,58 @@
/**
* BAD EXAMPLE: Entity Exposure
*
* Guardian should detect:
* ❌ Domain entity returned from controller
* ❌ No DTO layer
*
* Why bad:
* - Exposes internal structure
* - Breaking changes propagate to API
* - Can't version API independently
* - Security risk (password fields, etc.)
* - Violates Clean Architecture
*/
class User {
constructor(
public id: string,
public email: string,
public passwordHash: string,
public isAdmin: boolean,
) {}
}
export class BadUserController {
/**
* ❌ BAD: Returning domain entity directly!
*/
public async getUser(id: string): Promise<User> {
return new User(id, "user@example.com", "hashed_password_exposed!", true)
}
/**
* ❌ BAD: Accepting domain entity as input!
*/
public async updateUser(user: User): Promise<User> {
return user
}
}
/**
* ✅ GOOD VERSION:
*
* // application/dtos/UserResponseDto.ts
* export interface UserResponseDto {
* readonly id: string
* readonly email: string
* // NO password, NO internal fields
* }
*
* // infrastructure/controllers/UserController.ts
* export class UserController {
* async getUser(id: string): Promise<UserResponseDto> {
* const user = await this.getUserUseCase.execute(id)
* return UserMapper.toDto(user) // Convert to DTO!
* }
* }
*/