UNPKG

@lenne.tech/cli

Version:

lenne.Tech CLI: lt

852 lines (851 loc) 35.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.buildEffectiveMatrix = buildEffectiveMatrix; exports.calculateStats = calculateStats; exports.collectRoleEnums = collectRoleEnums; exports.detectSecurityGaps = detectSecurityGaps; exports.discoverModules = discoverModules; exports.extractDecoratorRoles = extractDecoratorRoles; exports.extractSecurityCheckInfo = extractSecurityCheckInfo; exports.formatRole = formatRole; exports.formatRolesDisplay = formatRolesDisplay; exports.generateMarkdownReport = generateMarkdownReport; exports.parseEndpointPermissions = parseEndpointPermissions; exports.parseFieldPermission = parseFieldPermission; exports.parseFilePermissions = parseFilePermissions; exports.parseRestrictedDecorator = parseRestrictedDecorator; exports.parseRolesDecorator = parseRolesDecorator; exports.resolveInheritedFields = resolveInheritedFields; exports.scanModule = scanModule; exports.scanObjects = scanObjects; exports.scanPermissions = scanPermissions; /** * Fallback permissions scanner for CLI usage. * * This is a standalone copy of the scanner from @lenne.tech/nest-server * (src/core/modules/permissions/permissions-scanner.ts). * * It is used as a fallback when the project's nest-server does not include * the permissions scanner (versions < 11.17.0). * * Prefer the nest-server scanner when available — it is the single source of truth. */ const fs_1 = require("fs"); const path_1 = require("path"); const ts_morph_1 = require("ts-morph"); // ───────────────────────────────────────────────────────────────────────────── // Exported functions (alphabetically sorted per ESLint perfectionist rules) // ───────────────────────────────────────────────────────────────────────────── function buildEffectiveMatrix(mod) { const allRoles = new Set(); for (const ctrl of mod.controllers) { for (const r of ctrl.classRoles) allRoles.add(r); for (const m of ctrl.methods) { for (const r of m.roles) allRoles.add(r); } } for (const res of mod.resolvers) { for (const r of res.classRoles) allRoles.add(r); for (const m of res.methods) { for (const r of m.roles) allRoles.add(r); } } const result = []; for (const role of [...allRoles].sort()) { const endpoints = []; for (const ctrl of mod.controllers) { for (const m of ctrl.methods) { const effective = m.roles.length > 0 ? m.roles : ctrl.classRoles; if (effective.includes(role)) { endpoints.push({ effectiveRoles: effective, method: m.httpMethod, name: m.name, source: 'Controller' }); } } } for (const res of mod.resolvers) { for (const m of res.methods) { const effective = m.roles.length > 0 ? m.roles : res.classRoles; if (effective.includes(role)) { endpoints.push({ effectiveRoles: effective, method: m.httpMethod, name: m.name, source: 'Resolver' }); } } } result.push({ endpoints, role }); } return result; } function calculateStats(modules, objects, warnings) { const warningsByType = { NO_RESTRICTION: 0, NO_ROLES: 0, NO_SECURITY_CHECK: 0, UNRESTRICTED_FIELD: 0, UNRESTRICTED_METHOD: 0, }; for (const w of warnings) { if (w.type in warningsByType) { warningsByType[w.type]++; } } const totalModels = modules.reduce((s, m) => s + m.models.length, 0); let totalMethods = 0; let methodsWithRoles = 0; for (const mod of modules) { for (const ctrl of mod.controllers) { for (const m of ctrl.methods) { totalMethods++; if (m.roles.length > 0 || ctrl.classRoles.length > 0) methodsWithRoles++; } } for (const res of mod.resolvers) { for (const m of res.methods) { totalMethods++; if (m.roles.length > 0 || res.classRoles.length > 0) methodsWithRoles++; } } } let modelsWithBothChecks = 0; for (const mod of modules) { for (const model of mod.models) { if (model.classRestriction.length > 0 && model.securityCheck) { modelsWithBothChecks++; } } } const endpointCoverage = totalMethods > 0 ? Math.round((methodsWithRoles / totalMethods) * 100) : 100; const securityCoverage = totalModels > 0 ? Math.round((modelsWithBothChecks / totalModels) * 100) : 100; return { endpointCoverage, securityCoverage, totalEndpoints: totalMethods, totalModels, totalModules: modules.length, totalSubObjects: objects.length, totalWarnings: warnings.length, warningsByType, }; } function collectRoleEnums(project, projectPath) { const enums = []; const enumPatterns = [ (0, path_1.join)(projectPath, 'src', 'server', 'common', 'enums'), (0, path_1.join)(projectPath, 'src', 'server', 'modules'), ]; for (const dir of enumPatterns) { try { const enumFiles = project.addSourceFilesAtPaths((0, path_1.join)(dir, '**', '*.enum.ts')); for (const sf of enumFiles) { for (const enumDecl of sf.getEnums()) { const enumName = enumDecl.getName(); if (enumName.toLowerCase().includes('role')) { const values = enumDecl.getMembers().map((m) => { var _a; return ({ key: m.getName(), value: ((_a = m.getValue()) === null || _a === void 0 ? void 0 : _a.toString()) || m.getName(), }); }); enums.push({ file: (0, path_1.relative)(projectPath, sf.getFilePath()), name: enumName, values, }); } } } } catch (_a) { // Directory may not exist } } return enums; } function detectSecurityGaps(modules, objects) { const warnings = []; for (const mod of modules) { for (const model of mod.models) { if (model.classRestriction.length === 0) { warnings.push({ details: `Model ${model.className} has no @Restricted class-level restriction`, file: model.filePath, module: mod.name, type: 'NO_RESTRICTION', }); } if (!model.securityCheck) { warnings.push({ details: `Model ${model.className} has no securityCheck override`, file: model.filePath, module: mod.name, type: 'NO_SECURITY_CHECK', }); } for (const field of model.fields) { if (field.roles === '*(none)*') { warnings.push({ details: `Field '${field.name}' has no role restriction`, file: model.filePath, module: mod.name, type: 'UNRESTRICTED_FIELD', }); } } } for (const input of mod.inputs) { for (const field of input.fields) { if (field.roles === '*(none)*') { warnings.push({ details: `Field '${field.name}' has no role restriction`, file: input.filePath, module: mod.name, type: 'UNRESTRICTED_FIELD', }); } } } for (const ctrl of mod.controllers) { if (ctrl.classRoles.length === 0) { warnings.push({ details: `Controller ${ctrl.className} has no @Roles class-level restriction`, file: ctrl.filePath, module: mod.name, type: 'NO_ROLES', }); } for (const method of ctrl.methods) { if (method.roles.length === 0 && ctrl.classRoles.length === 0) { warnings.push({ details: `Method '${method.name}' has no @Roles and class has no @Roles`, file: ctrl.filePath, module: mod.name, type: 'UNRESTRICTED_METHOD', }); } } } for (const res of mod.resolvers) { if (res.classRoles.length === 0) { warnings.push({ details: `Resolver ${res.className} has no @Roles class-level restriction`, file: res.filePath, module: mod.name, type: 'NO_ROLES', }); } for (const method of res.methods) { if (method.roles.length === 0 && res.classRoles.length === 0) { warnings.push({ details: `Method '${method.name}' has no @Roles and class has no @Roles`, file: res.filePath, module: mod.name, type: 'UNRESTRICTED_METHOD', }); } } } } for (const obj of objects) { for (const field of obj.fields) { if (field.roles === '*(none)*') { warnings.push({ details: `Field '${field.name}' has no role restriction`, file: obj.filePath, module: 'objects', type: 'UNRESTRICTED_FIELD', }); } } } return warnings; } function discoverModules(modulesDir) { if (!(0, fs_1.existsSync)(modulesDir)) return []; return (0, fs_1.readdirSync)(modulesDir) .filter((item) => (0, fs_1.statSync)((0, path_1.join)(modulesDir, item)).isDirectory()) .sort(); } function extractDecoratorRoles(decoratorArgs) { const roles = []; for (const arg of decoratorArgs) { const cleaned = arg.trim(); if (cleaned.startsWith('[')) { const inner = cleaned.slice(1, -1); for (const item of inner.split(',')) { roles.push(formatRole(item.trim())); } } else { roles.push(formatRole(cleaned)); } } return roles; } function extractSecurityCheckInfo(classDecl) { const method = classDecl.getMethod('securityCheck'); if (!method) return undefined; const body = method.getBodyText() || ''; const fieldsStripped = []; const deleteMatches = body.matchAll(/delete\s+\w+\.(\w+)/g); for (const m of deleteMatches) fieldsStripped.push(m[1]); const arrayMatches = body.matchAll(/(?:fieldsToRemove|removeFields|stripFields|fieldsToStrip)\s*=\s*\[([^\]]+)\]/g); for (const m of arrayMatches) { fieldsStripped.push(...m[1].split(',').map((f) => f.trim().replace(/['"]/g, ''))); } const graphqlFieldMatches = body.matchAll(/(?:removeKeys|filterKeys)\s*\([^,]*,\s*\[([^\]]+)\]/g); for (const m of graphqlFieldMatches) { fieldsStripped.push(...m[1].split(',').map((f) => f.trim().replace(/['"]/g, ''))); } const returnsUndefined = body.includes('return undefined') || body.includes('return null'); const summaryParts = ['Present']; if (fieldsStripped.length > 0) summaryParts.push(`Strips fields: ${fieldsStripped.join(', ')}`); if (returnsUndefined) summaryParts.push('May return undefined'); return { fieldsStripped: [...new Set(fieldsStripped)], returnsUndefined, summary: summaryParts.join('. ') }; } function formatRole(role) { if (!role) return ''; const dotIndex = role.lastIndexOf('.'); return dotIndex >= 0 ? role.substring(dotIndex + 1) : role; } function formatRolesDisplay(roles) { if (roles.length === 0) return '*(none)*'; if (roles.length === 1) return `\`${roles[0]}\``; return roles.map((r) => `\`${r}\``).join(', '); } function generateMarkdownReport(report, projectPath) { const lines = []; // Header lines.push('# Permissions Report'); lines.push(''); lines.push(`> Generated: ${report.generated}`); if (projectPath) lines.push(`> Project: ${projectPath}`); lines.push(`> Modules: ${report.stats.totalModules} | Models: ${report.stats.totalModels} | Endpoints: ${report.stats.totalEndpoints} | SubObjects: ${report.stats.totalSubObjects}`); lines.push(`> Warnings: ${report.stats.totalWarnings} | Endpoint Coverage: ${report.stats.endpointCoverage}% | Security Coverage: ${report.stats.securityCoverage}%`); lines.push(''); // Table of contents lines.push('## Table of Contents'); lines.push(''); lines.push('- [Role Index](#role-index)'); lines.push('- [Summary](#summary)'); lines.push('- [Warnings](#warnings)'); for (const mod of report.modules) { const anchor = mod.name.toLowerCase().replace(/[^a-z0-9-]/g, '-'); lines.push(`- [Module: ${mod.name}](#module-${anchor})`); } if (report.objects.length > 0) { lines.push('- [SubObjects](#subobjects)'); } lines.push(''); // Role index lines.push('## Role Index'); lines.push(''); if (report.roleEnums.length > 0) { lines.push('| Enum | Value | Type |'); lines.push('|------|-------|------|'); for (const enumInfo of report.roleEnums) { for (const v of enumInfo.values) { const isSystem = v.key.startsWith('S_'); lines.push(`| ${enumInfo.name}.${v.key} | ${isSystem ? '*(system)*' : `\`${v.value}\``} | ${isSystem ? 'System' : 'Real'} |`); } } } else { lines.push('*No role enums found.*'); } lines.push(''); // Summary table lines.push('## Summary'); lines.push(''); lines.push('| Module | Models | Inputs | Outputs | Controllers | Resolvers | Warnings |'); lines.push('|--------|--------|--------|---------|-------------|-----------|----------|'); for (const mod of report.modules) { const modWarnings = report.warnings.filter((w) => w.module === mod.name).length; lines.push(`| ${mod.name} | ${mod.models.length} | ${mod.inputs.length} | ${mod.outputs.length} | ${mod.controllers.length} | ${mod.resolvers.length} | ${modWarnings} |`); } lines.push(''); if (report.stats.totalWarnings > 0) { lines.push('### Warnings by Type'); lines.push(''); lines.push('| Type | Count |'); lines.push('|------|-------|'); for (const [type, count] of Object.entries(report.stats.warningsByType)) { if (count > 0) lines.push(`| ${type} | ${count} |`); } lines.push(''); } // Warnings lines.push('## Warnings'); lines.push(''); if (report.warnings.length > 0) { lines.push('| # | Module | File | Type | Details |'); lines.push('|---|--------|------|------|---------|'); report.warnings.forEach((w, i) => { const fileName = w.file.split('/').pop() || w.file; lines.push(`| ${i + 1} | ${w.module} | ${fileName} | ${w.type} | ${w.details} |`); }); } else { lines.push('*No warnings found.*'); } lines.push(''); // Module details for (const mod of report.modules) { lines.push('---'); lines.push(''); lines.push(`## Module: ${mod.name}`); lines.push(''); // Models for (const model of mod.models) { lines.push(`### Model: ${model.className}`); lines.push(`- **File:** \`${model.filePath}\``); if (model.extendsClass) lines.push(`- **Extends:** \`${model.extendsClass}\``); lines.push(`- **Class Restriction:** ${model.classRestriction.length > 0 ? model.classRestriction.map((r) => `\`${r}\``).join(', ') : '*(none)*'}`); if (model.securityCheck) { lines.push(`- **securityCheck:** ${model.securityCheck.summary}`); } else { lines.push('- **securityCheck:** Not present'); } lines.push(''); if (model.fields.length > 0) { lines.push('| Field | Roles | Source |'); lines.push('|-------|-------|--------|'); for (const field of model.fields) { const source = field.inherited ? 'inherited' : 'local'; lines.push(`| ${field.name} | ${field.roles} | ${source} |`); } } lines.push(''); } // Inputs for (const input of mod.inputs) { lines.push(`### Input: ${input.className}`); lines.push(`- **File:** \`${input.filePath}\``); if (input.extendsClass) lines.push(`- **Extends:** \`${input.extendsClass}\``); lines.push(`- **Class Restriction:** ${input.classRestriction.length > 0 ? input.classRestriction.map((r) => `\`${r}\``).join(', ') : '*(none)*'}`); lines.push(''); if (input.fields.length > 0) { lines.push('| Field | Roles |'); lines.push('|-------|-------|'); for (const field of input.fields) { lines.push(`| ${field.name} | ${field.roles} |`); } } lines.push(''); } // Outputs for (const output of mod.outputs) { lines.push(`### Output: ${output.className}`); lines.push(`- **File:** \`${output.filePath}\``); if (output.extendsClass) lines.push(`- **Extends:** \`${output.extendsClass}\``); lines.push(''); if (output.fields.length > 0) { lines.push('| Field | Roles |'); lines.push('|-------|-------|'); for (const field of output.fields) { lines.push(`| ${field.name} | ${field.roles} |`); } } lines.push(''); } // Controllers for (const ctrl of mod.controllers) { lines.push(`### Controller: ${ctrl.className}`); lines.push(`- **File:** \`${ctrl.filePath}\``); lines.push(`- **Class Roles:** ${ctrl.classRoles.length > 0 ? ctrl.classRoles.map((r) => `\`${r}\``).join(', ') : '*(none)*'}`); lines.push(''); if (ctrl.methods.length > 0) { lines.push('| Method | HTTP | Route | Roles | Effective |'); lines.push('|--------|------|-------|-------|-----------|'); for (const m of ctrl.methods) { const effective = m.roles.length > 0 ? formatRolesDisplay(m.roles) : `${formatRolesDisplay(ctrl.classRoles)} (class)`; lines.push(`| ${m.name} | ${m.httpMethod} | ${m.route || '/'} | ${formatRolesDisplay(m.roles)} | ${effective} |`); } } lines.push(''); } // Resolvers for (const res of mod.resolvers) { lines.push(`### Resolver: ${res.className}`); lines.push(`- **File:** \`${res.filePath}\``); lines.push(`- **Class Roles:** ${res.classRoles.length > 0 ? res.classRoles.map((r) => `\`${r}\``).join(', ') : '*(none)*'}`); lines.push(''); if (res.methods.length > 0) { lines.push('| Method | Type | Roles | Effective |'); lines.push('|--------|------|-------|-----------|'); for (const m of res.methods) { const effective = m.roles.length > 0 ? formatRolesDisplay(m.roles) : `${formatRolesDisplay(res.classRoles)} (class)`; lines.push(`| ${m.name} | ${m.httpMethod} | ${formatRolesDisplay(m.roles)} | ${effective} |`); } } lines.push(''); } // Effective matrix const matrix = buildEffectiveMatrix(mod); if (matrix.length > 0) { lines.push(`### Effective Permissions: ${mod.name}`); lines.push(''); lines.push('| Role | Endpoint Access |'); lines.push('|------|-----------------|'); for (const entry of matrix) { const endpointList = entry.endpoints.map((e) => `${e.method} ${e.name}`).join(', '); lines.push(`| \`${entry.role}\` | ${endpointList || '*(none)*'} |`); } lines.push(''); } } // SubObjects if (report.objects.length > 0) { lines.push('---'); lines.push(''); lines.push('## SubObjects'); lines.push(''); for (const obj of report.objects) { lines.push(`### ${obj.className}`); lines.push(`- **File:** \`${obj.filePath}\``); if (obj.extendsClass) lines.push(`- **Extends:** \`${obj.extendsClass}\``); lines.push(`- **Class Restriction:** ${obj.classRestriction.length > 0 ? obj.classRestriction.map((r) => `\`${r}\``).join(', ') : '*(none)*'}`); lines.push(''); if (obj.fields.length > 0) { lines.push('| Field | Roles | Source |'); lines.push('|-------|-------|--------|'); for (const field of obj.fields) { const source = field.inherited ? 'inherited' : 'local'; lines.push(`| ${field.name} | ${field.roles} | ${source} |`); } } lines.push(''); } } return lines.join('\n'); } function parseEndpointPermissions(sourceFile, filePath) { const classes = sourceFile.getClasses(); if (classes.length === 0) return undefined; const classDecl = classes[0]; const className = classDecl.getName() || 'Unknown'; const classRoles = parseRolesDecorator(classDecl); let controllerPrefix = ''; const controllerDeco = classDecl.getDecorator('Controller'); if (controllerDeco) { const args = controllerDeco.getArguments(); if (args.length > 0) controllerPrefix = args[0].getText().replace(/['"]/g, ''); } const methods = []; for (const method of classDecl.getMethods()) { const methodName = method.getName(); if (methodName.startsWith('_') || ['onModuleDestroy', 'onModuleInit'].includes(methodName)) continue; const methodRoles = parseRolesDecorator(method); for (const httpDeco of ['Delete', 'Get', 'Patch', 'Post', 'Put']) { const deco = method.getDecorator(httpDeco); if (deco) { const args = deco.getArguments(); const route = args.length > 0 ? args[0].getText().replace(/['"]/g, '') : '/'; const fullRoute = controllerPrefix ? `/${controllerPrefix}/${route}`.replace(/\/+/g, '/') : `/${route}`.replace(/\/+/g, '/'); methods.push({ httpMethod: httpDeco.toUpperCase(), name: methodName, roles: methodRoles, route: fullRoute }); } } for (const gqlDeco of ['Mutation', 'Query', 'Subscription']) { const deco = method.getDecorator(gqlDeco); if (deco) { methods.push({ httpMethod: gqlDeco, name: methodName, roles: methodRoles }); } } } return { className, classRoles, filePath, methods }; } function parseFieldPermission(prop) { var _a, _b; const fieldName = prop.getName(); let roles = '*(none)*'; let description; const unifiedField = prop.getDecorator('UnifiedField'); if (unifiedField) { const args = unifiedField.getArguments(); if (args.length > 0) { const optionsArg = args[0]; if (optionsArg.getKind() === ts_morph_1.SyntaxKind.ObjectLiteralExpression) { const objLit = optionsArg.asKind(ts_morph_1.SyntaxKind.ObjectLiteralExpression); const rolesProp = objLit === null || objLit === void 0 ? void 0 : objLit.getProperty('roles'); if (rolesProp) { const init = (_a = rolesProp.asKind(ts_morph_1.SyntaxKind.PropertyAssignment)) === null || _a === void 0 ? void 0 : _a.getInitializer(); if (init) { const rolesText = init.getText(); if (rolesText.startsWith('[')) { const inner = rolesText.slice(1, -1); const roleList = inner.split(',').map((r) => formatRole(r.trim())); roles = roleList.map((r) => `\`${r}\``).join(', '); } else { roles = `\`${formatRole(rolesText)}\``; } } } const descProp = objLit === null || objLit === void 0 ? void 0 : objLit.getProperty('description'); if (descProp) { const init = (_b = descProp.asKind(ts_morph_1.SyntaxKind.PropertyAssignment)) === null || _b === void 0 ? void 0 : _b.getInitializer(); if (init) description = init.getText().replace(/^['"]|['"]$/g, ''); } } } } if (roles === '*(none)*') { const restricted = prop.getDecorator('Restricted'); if (restricted) { const args = restricted.getArguments().map((a) => a.getText()); if (args.length > 0) { const roleList = extractDecoratorRoles(args); roles = roleList.map((r) => `\`${r}\``).join(', '); } } } return { description, name: fieldName, roles }; } function parseFilePermissions(sourceFile, filePath, isModel) { var _a; const classes = sourceFile.getClasses(); if (classes.length === 0) return undefined; const classDecl = classes[0]; const className = classDecl.getName() || 'Unknown'; const extendsExpr = classDecl.getExtends(); const extendsClass = ((_a = extendsExpr === null || extendsExpr === void 0 ? void 0 : extendsExpr.getText()) === null || _a === void 0 ? void 0 : _a.replace(/<.*>/, '')) || undefined; const classRestriction = parseRestrictedDecorator(classDecl); const securityCheck = isModel ? extractSecurityCheckInfo(classDecl) : undefined; const fields = []; for (const prop of classDecl.getProperties()) { fields.push(parseFieldPermission(prop)); } return { className, classRestriction, extendsClass, fields, filePath, securityCheck }; } function parseRestrictedDecorator(node) { const restricted = node.getDecorator('Restricted'); if (!restricted) return []; const args = restricted.getArguments().map((a) => a.getText()); return extractDecoratorRoles(args); } function parseRolesDecorator(node) { const roles = node.getDecorator('Roles'); if (!roles) return []; const args = roles.getArguments().map((a) => a.getText()); return extractDecoratorRoles(args); } function resolveInheritedFields(project, classDecl) { const inherited = []; const extendsExpr = classDecl.getExtends(); if (!extendsExpr) return inherited; const baseClassName = extendsExpr.getText().replace(/<.*>/, ''); for (const sf of project.getSourceFiles()) { for (const cls of sf.getClasses()) { if (cls.getName() === baseClassName) { for (const prop of cls.getProperties()) { const field = parseFieldPermission(prop); field.inherited = true; inherited.push(field); } const parentFields = resolveInheritedFields(project, cls); inherited.push(...parentFields); return inherited; } } } return inherited; } function scanModule(project, modulesDir, moduleName, projectPath) { const moduleDir = (0, path_1.join)(modulesDir, moduleName); const result = { controllers: [], inputs: [], models: [], name: moduleName, outputs: [], resolvers: [], }; // Models for (const file of listDir(moduleDir).filter((f) => f.endsWith('.model.ts'))) { try { const sf = project.addSourceFileAtPath((0, path_1.join)(moduleDir, file)); const perms = parseFilePermissions(sf, (0, path_1.relative)(projectPath, (0, path_1.join)(moduleDir, file)), true); if (perms) { const classDecl = sf.getClasses()[0]; if (classDecl) { const inheritedFields = resolveInheritedFields(project, classDecl); const localNames = new Set(perms.fields.map((f) => f.name)); for (const iField of inheritedFields) { if (!localNames.has(iField.name)) perms.fields.push(iField); } } result.models.push(perms); } } catch (_a) { /* skip */ } } // Inputs const inputDir = (0, path_1.join)(moduleDir, 'inputs'); for (const file of listDir(inputDir).filter((f) => f.endsWith('.input.ts'))) { try { const sf = project.addSourceFileAtPath((0, path_1.join)(inputDir, file)); const perms = parseFilePermissions(sf, (0, path_1.relative)(projectPath, (0, path_1.join)(inputDir, file)), false); if (perms) result.inputs.push(perms); } catch (_b) { /* skip */ } } // Outputs const outputDir = (0, path_1.join)(moduleDir, 'outputs'); for (const file of listDir(outputDir).filter((f) => f.endsWith('.output.ts'))) { try { const sf = project.addSourceFileAtPath((0, path_1.join)(outputDir, file)); const perms = parseFilePermissions(sf, (0, path_1.relative)(projectPath, (0, path_1.join)(outputDir, file)), false); if (perms) result.outputs.push(perms); } catch (_c) { /* skip */ } } // Controllers for (const file of listDir(moduleDir).filter((f) => f.endsWith('.controller.ts'))) { try { const sf = project.addSourceFileAtPath((0, path_1.join)(moduleDir, file)); const perms = parseEndpointPermissions(sf, (0, path_1.relative)(projectPath, (0, path_1.join)(moduleDir, file))); if (perms) result.controllers.push(perms); } catch (_d) { /* skip */ } } // Resolvers for (const file of listDir(moduleDir).filter((f) => f.endsWith('.resolver.ts'))) { try { const sf = project.addSourceFileAtPath((0, path_1.join)(moduleDir, file)); const perms = parseEndpointPermissions(sf, (0, path_1.relative)(projectPath, (0, path_1.join)(moduleDir, file))); if (perms) result.resolvers.push(perms); } catch (_e) { /* skip */ } } return result; } function scanObjects(project, objectsDir, projectPath) { const objects = []; if (!(0, fs_1.existsSync)(objectsDir)) return objects; for (const dir of listDir(objectsDir)) { const dirPath = (0, path_1.join)(objectsDir, dir); try { if (!(0, fs_1.statSync)(dirPath).isDirectory()) continue; } catch (_a) { continue; } for (const file of listDir(dirPath).filter((f) => f.endsWith('.object.ts'))) { try { const sf = project.addSourceFileAtPath((0, path_1.join)(dirPath, file)); const perms = parseFilePermissions(sf, (0, path_1.relative)(projectPath, (0, path_1.join)(dirPath, file)), false); if (perms) objects.push(perms); } catch (_b) { /* skip */ } } } return objects; } function scanPermissions(projectPath, logger) { const log = (logger === null || logger === void 0 ? void 0 : logger.log) || (() => { }); const warn = (logger === null || logger === void 0 ? void 0 : logger.warn) || (() => { }); log('Scanning permissions...'); const project = new ts_morph_1.Project({ compilerOptions: { allowJs: true }, skipAddingFilesFromTsConfig: true }); const modulesDir = (0, path_1.join)(projectPath, 'src', 'server', 'modules'); const objectsDir = (0, path_1.join)(projectPath, 'src', 'server', 'common', 'objects'); // Preload nest-server base classes once (used by resolveInheritedFields for all models) preloadBaseClasses(project, projectPath); const roleEnums = collectRoleEnums(project, projectPath); const moduleNames = discoverModules(modulesDir); const modules = []; for (const name of moduleNames) { try { modules.push(scanModule(project, modulesDir, name, projectPath)); } catch (error) { warn(`Failed to scan module '${name}': ${error}`); } } const objects = scanObjects(project, objectsDir, projectPath); const warnings = detectSecurityGaps(modules, objects); const stats = calculateStats(modules, objects, warnings); log(`Scan complete: ${modules.length} modules, ${objects.length} objects, ${warnings.length} warnings`); return { generated: new Date().toISOString(), modules, objects, roleEnums, stats, warnings, }; } // ───────────────────────────────────────────────────────────────────────────── // Internal helpers // ───────────────────────────────────────────────────────────────────────────── function listDir(dir) { try { return (0, fs_1.readdirSync)(dir); } catch (_a) { return []; } } function preloadBaseClasses(project, projectPath) { const nestServerPaths = [ (0, path_1.join)(projectPath, 'node_modules', '@lenne.tech', 'nest-server', 'src'), (0, path_1.join)(projectPath, 'node_modules', '@lenne.tech', 'nest-server', 'dist'), ]; for (const basePath of nestServerPaths) { try { if ((0, fs_1.existsSync)(basePath)) { project.addSourceFilesAtPaths((0, path_1.join)(basePath, '**', '*.ts')); } } catch (_a) { // Base path not available } } }