@atlashub/smartstack-cli 3.16.0 → 3.17.0

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.
@@ -25774,7 +25774,14 @@ var init_types3 = __esm({
25774
25774
  dbContext: external_exports.enum(["core", "extensions"]).default("core"),
25775
25775
  baseNamespace: external_exports.string().optional(),
25776
25776
  smartStackVersion: external_exports.string().optional(),
25777
- initialized: external_exports.string().optional()
25777
+ initialized: external_exports.string().optional(),
25778
+ paths: external_exports.object({
25779
+ api: external_exports.string().optional(),
25780
+ domain: external_exports.string().optional(),
25781
+ application: external_exports.string().optional(),
25782
+ infrastructure: external_exports.string().optional(),
25783
+ web: external_exports.string().optional()
25784
+ }).optional()
25778
25785
  });
25779
25786
  SmartStackConfigSchema = external_exports.object({
25780
25787
  projectPath: external_exports.string(),
@@ -25848,7 +25855,7 @@ var init_types3 = __esm({
25848
25855
  });
25849
25856
  ValidateConventionsInputSchema = external_exports.object({
25850
25857
  path: external_exports.string().optional().describe("Project path to validate (default: SmartStack.app path)"),
25851
- checks: external_exports.array(external_exports.enum(["tables", "migrations", "services", "namespaces", "entities", "tenants", "controllers", "layouts", "tabs", "hierarchies", "protected-actions", "all"])).default(["all"]).describe("Types of checks to perform")
25858
+ checks: external_exports.array(external_exports.enum(["tables", "migrations", "services", "namespaces", "entities", "tenants", "controllers", "layouts", "tabs", "hierarchies", "protected-actions", "permissions", "frontend-routes", "feature-json", "all"])).default(["all"]).describe("Types of checks to perform")
25852
25859
  });
25853
25860
  CheckMigrationsInputSchema = external_exports.object({
25854
25861
  projectPath: external_exports.string().optional().describe("EF Core project path"),
@@ -25947,6 +25954,7 @@ var init_types3 = __esm({
25947
25954
  "authorization",
25948
25955
  "dangerous-functions",
25949
25956
  "input-validation",
25957
+ "guid-empty",
25950
25958
  "xss",
25951
25959
  "csrf",
25952
25960
  "logging-sensitive",
@@ -26578,21 +26586,36 @@ async function detectProject(projectPath) {
26578
26586
  }
26579
26587
  async function findSmartStackStructure(projectPath) {
26580
26588
  const structure = { root: projectPath };
26589
+ const configPath = path7.join(projectPath, ".smartstack", "config.json");
26590
+ if (await fileExists(configPath)) {
26591
+ try {
26592
+ const configContent = JSON.parse(await readText(configPath));
26593
+ if (configContent.paths) {
26594
+ const paths = configContent.paths;
26595
+ if (paths.api) structure.api = path7.resolve(projectPath, paths.api);
26596
+ if (paths.domain) structure.domain = path7.resolve(projectPath, paths.domain);
26597
+ if (paths.application) structure.application = path7.resolve(projectPath, paths.application);
26598
+ if (paths.infrastructure) structure.infrastructure = path7.resolve(projectPath, paths.infrastructure);
26599
+ if (paths.web) structure.web = path7.resolve(projectPath, paths.web);
26600
+ }
26601
+ } catch {
26602
+ }
26603
+ }
26581
26604
  const csprojFiles = await findCsprojFiles(projectPath);
26582
26605
  for (const csproj of csprojFiles) {
26583
26606
  const projectName = path7.basename(csproj, ".csproj").toLowerCase();
26584
26607
  const projectDir = path7.dirname(csproj);
26585
- if (projectName.includes("domain")) {
26608
+ if (!structure.domain && projectName.includes("domain")) {
26586
26609
  structure.domain = projectDir;
26587
- } else if (projectName.includes("application")) {
26610
+ } else if (!structure.application && projectName.includes("application")) {
26588
26611
  structure.application = projectDir;
26589
- } else if (projectName.includes("infrastructure")) {
26612
+ } else if (!structure.infrastructure && projectName.includes("infrastructure")) {
26590
26613
  structure.infrastructure = projectDir;
26591
26614
  } else if (projectName.includes("api.core")) {
26592
- structure.apiCore = projectDir;
26593
- structure.api = projectDir;
26615
+ if (!structure.apiCore) structure.apiCore = projectDir;
26616
+ if (!structure.api) structure.api = projectDir;
26594
26617
  } else if (projectName.includes("api") && !projectName.includes("api.core")) {
26595
- structure.apiExtensions = projectDir;
26618
+ if (!structure.apiExtensions) structure.apiExtensions = projectDir;
26596
26619
  if (!structure.api) {
26597
26620
  structure.api = projectDir;
26598
26621
  }
@@ -26604,9 +26627,11 @@ async function findSmartStackStructure(projectPath) {
26604
26627
  structure.migrations = migrationsPath;
26605
26628
  }
26606
26629
  }
26607
- const webFolder = await findWebProjectFolder(projectPath);
26608
- if (webFolder) {
26609
- structure.web = webFolder;
26630
+ if (!structure.web) {
26631
+ const webFolder = await findWebProjectFolder(projectPath);
26632
+ if (webFolder) {
26633
+ structure.web = webFolder;
26634
+ }
26610
26635
  }
26611
26636
  return structure;
26612
26637
  }
@@ -26642,7 +26667,7 @@ import path8 from "path";
26642
26667
  async function handleValidateConventions(args, config2) {
26643
26668
  const input = ValidateConventionsInputSchema.parse(args);
26644
26669
  const projectPath = input.path || config2.smartstack.projectPath;
26645
- const checks = input.checks.includes("all") ? ["tables", "migrations", "services", "namespaces", "entities", "tenants", "controllers", "layouts", "tabs", "hierarchies", "protected-actions"] : input.checks;
26670
+ const checks = input.checks.includes("all") ? ["tables", "migrations", "services", "namespaces", "entities", "tenants", "controllers", "layouts", "tabs", "hierarchies", "protected-actions", "permissions", "frontend-routes", "feature-json"] : input.checks;
26646
26671
  logger.info("Validating conventions", { projectPath, checks });
26647
26672
  const result = {
26648
26673
  valid: true,
@@ -26684,6 +26709,15 @@ async function handleValidateConventions(args, config2) {
26684
26709
  if (checks.includes("protected-actions")) {
26685
26710
  await validateProtectedActions(structure, config2, result);
26686
26711
  }
26712
+ if (checks.includes("permissions")) {
26713
+ await validatePermissionSeeding(structure, config2, result);
26714
+ }
26715
+ if (checks.includes("frontend-routes")) {
26716
+ await validateFrontendRoutes(structure, config2, result);
26717
+ }
26718
+ if (checks.includes("feature-json")) {
26719
+ await validateFeatureJson(structure, config2, result);
26720
+ }
26687
26721
  result.valid = result.errors.length === 0;
26688
26722
  result.summary = generateSummary(result, checks);
26689
26723
  return formatResult(result);
@@ -27066,6 +27100,7 @@ async function validateControllerRoutes(structure, _config, result) {
27066
27100
  const navRouteMatch = content.match(/\[NavRoute\s*\(\s*"([^"]+)"(?:\s*,\s*Suffix\s*=\s*"([^"]+)")?\s*\)\]/);
27067
27101
  if (navRouteMatch) {
27068
27102
  const routePath = navRouteMatch[1];
27103
+ const suffix = navRouteMatch[2];
27069
27104
  const parts = routePath.split(".");
27070
27105
  if (parts.length < 2) {
27071
27106
  result.warnings.push({
@@ -27086,6 +27121,28 @@ async function validateControllerRoutes(structure, _config, result) {
27086
27121
  suggestion: 'NavRoute paths must be lowercase (e.g., "platform.administration.users")'
27087
27122
  });
27088
27123
  }
27124
+ const expectedRoute = `api/${routePath.replace(/\./g, "/")}${suffix ? `/${suffix}` : ""}`;
27125
+ const routeAttrMatch = content.match(/\[Route\s*\(\s*"([^"]+)"\s*\)\]/);
27126
+ if (routeAttrMatch) {
27127
+ const actualRoute = routeAttrMatch[1];
27128
+ if (actualRoute !== expectedRoute) {
27129
+ result.errors.push({
27130
+ type: "error",
27131
+ category: "controllers",
27132
+ message: `Controller "${fileName}" has [Route("${actualRoute}")] that doesn't match NavRoute "${routePath}". Expected [Route("${expectedRoute}")]`,
27133
+ file: path8.relative(structure.root, file),
27134
+ suggestion: `Change [Route] to [Route("${expectedRoute}")] to match the NavRoute convention`
27135
+ });
27136
+ }
27137
+ } else {
27138
+ result.warnings.push({
27139
+ type: "warning",
27140
+ category: "controllers",
27141
+ message: `Controller "${fileName}" has [NavRoute] but no explicit [Route] attribute`,
27142
+ file: path8.relative(structure.root, file),
27143
+ suggestion: `Add [Route("${expectedRoute}")] for deterministic routing`
27144
+ });
27145
+ }
27089
27146
  }
27090
27147
  } else if (hasHardcodedRoute) {
27091
27148
  hardcodedRouteCount++;
@@ -27098,6 +27155,44 @@ async function validateControllerRoutes(structure, _config, result) {
27098
27155
  });
27099
27156
  }
27100
27157
  }
27158
+ const routesByDirectory = /* @__PURE__ */ new Map();
27159
+ for (const file of controllerFiles) {
27160
+ const content = await readText(file);
27161
+ const fileName = path8.basename(file, ".cs");
27162
+ if (systemControllers.includes(fileName)) continue;
27163
+ const routeAttrMatch = content.match(/\[Route\s*\(\s*"([^"]+)"\s*\)\]/);
27164
+ if (!routeAttrMatch) continue;
27165
+ const routeValue = routeAttrMatch[1];
27166
+ const relativePath = path8.relative(structure.api, file);
27167
+ const dirParts = path8.dirname(relativePath).split(path8.sep);
27168
+ const moduleDir = dirParts.slice(0, Math.min(dirParts.length, 3)).join("/");
27169
+ if (!routesByDirectory.has(moduleDir)) {
27170
+ routesByDirectory.set(moduleDir, []);
27171
+ }
27172
+ routesByDirectory.get(moduleDir).push({
27173
+ controller: fileName,
27174
+ route: routeValue,
27175
+ file: path8.relative(structure.root, file)
27176
+ });
27177
+ }
27178
+ for (const [moduleDir, controllers] of routesByDirectory) {
27179
+ if (controllers.length < 2) continue;
27180
+ const routeBases = controllers.map((c) => {
27181
+ const parts = c.route.split("/");
27182
+ return parts.slice(0, -1).join("/");
27183
+ });
27184
+ const uniqueBases = [...new Set(routeBases)];
27185
+ if (uniqueBases.length > 1) {
27186
+ const routeList = controllers.map((c) => `${c.controller}: "${c.route}"`).join(", ");
27187
+ result.errors.push({
27188
+ type: "error",
27189
+ category: "controllers",
27190
+ message: `Inconsistent route bases in ${moduleDir}: [${uniqueBases.join("] vs [")}]`,
27191
+ file: controllers[0].file,
27192
+ suggestion: `All controllers in the same module should share the same route base. Found: ${routeList}`
27193
+ });
27194
+ }
27195
+ }
27101
27196
  const totalControllers = controllerFiles.length;
27102
27197
  const businessControllers = totalControllers - systemControllerCount;
27103
27198
  const navRoutePercentage = businessControllers > 0 ? Math.round(navRouteCount / businessControllers * 100) : 0;
@@ -27553,6 +27648,344 @@ async function validateProtectedActions(structure, _config, result) {
27553
27648
  }
27554
27649
  }
27555
27650
  }
27651
+ async function validatePermissionSeeding(structure, _config, result) {
27652
+ const searchPaths = [structure.infrastructure, structure.application].filter(Boolean);
27653
+ if (searchPaths.length === 0) {
27654
+ result.warnings.push({
27655
+ type: "warning",
27656
+ category: "permissions",
27657
+ message: "Infrastructure/Application projects not found, skipping permission validation"
27658
+ });
27659
+ return;
27660
+ }
27661
+ let enumParseCount = 0;
27662
+ let stringActionCount = 0;
27663
+ let typeSafeCount = 0;
27664
+ for (const searchPath of searchPaths) {
27665
+ const seedFiles = await findFiles("**/*Seed*.cs", { cwd: searchPath });
27666
+ const permFiles = await findFiles("**/*Permission*.cs", { cwd: searchPath });
27667
+ const providerFiles = await findFiles("**/*Provider*.cs", { cwd: searchPath });
27668
+ const allFiles = [.../* @__PURE__ */ new Set([...seedFiles, ...permFiles, ...providerFiles])];
27669
+ for (const file of allFiles) {
27670
+ const content = await readText(file);
27671
+ const relPath = path8.relative(structure.root, file);
27672
+ const lines = content.split("\n");
27673
+ for (let i = 0; i < lines.length; i++) {
27674
+ const line = lines[i];
27675
+ const trimmed = line.trim();
27676
+ if (trimmed.startsWith("//") || trimmed.startsWith("*") || trimmed.startsWith("/*")) {
27677
+ continue;
27678
+ }
27679
+ const enumParseMatch = line.match(/Enum\.Parse\s*<\s*PermissionAction\s*>\s*\(\s*"([^"]*)"\s*\)/);
27680
+ if (enumParseMatch) {
27681
+ enumParseCount++;
27682
+ const parsedValue = enumParseMatch[1];
27683
+ const isValid2 = VALID_PERMISSION_ACTIONS.includes(parsedValue);
27684
+ result.errors.push({
27685
+ type: "error",
27686
+ category: "permissions",
27687
+ message: `Enum.Parse<PermissionAction>("${parsedValue}") is an anti-pattern${!isValid2 ? ` \u2014 "${parsedValue}" is NOT a valid PermissionAction value` : ""}`,
27688
+ file: relPath,
27689
+ line: i + 1,
27690
+ suggestion: `Use PermissionAction.${isValid2 ? parsedValue : "Read"} directly (compile-time safe). Valid values: ${VALID_PERMISSION_ACTIONS.join(", ")}`
27691
+ });
27692
+ }
27693
+ const enumParseTypeofMatch = line.match(/\(\s*PermissionAction\s*\)\s*Enum\.Parse\s*\(\s*typeof\s*\(\s*PermissionAction\s*\)\s*,\s*"([^"]*)"\s*\)/);
27694
+ if (enumParseTypeofMatch) {
27695
+ enumParseCount++;
27696
+ const parsedValue = enumParseTypeofMatch[1];
27697
+ const isValid2 = VALID_PERMISSION_ACTIONS.includes(parsedValue);
27698
+ result.errors.push({
27699
+ type: "error",
27700
+ category: "permissions",
27701
+ message: `Enum.Parse(typeof(PermissionAction), "${parsedValue}") is an anti-pattern${!isValid2 ? ` \u2014 "${parsedValue}" is NOT a valid value` : ""}`,
27702
+ file: relPath,
27703
+ line: i + 1,
27704
+ suggestion: `Use PermissionAction.${isValid2 ? parsedValue : "Read"} directly. Valid values: ${VALID_PERMISSION_ACTIONS.join(", ")}`
27705
+ });
27706
+ }
27707
+ if (/Action\s*=\s*"[^"]*"/.test(line) && /[Pp]ermission/.test(content.substring(0, content.indexOf(line)))) {
27708
+ const stringMatch = line.match(/Action\s*=\s*"([^"]*)"/);
27709
+ if (stringMatch) {
27710
+ stringActionCount++;
27711
+ result.warnings.push({
27712
+ type: "warning",
27713
+ category: "permissions",
27714
+ message: `Permission Action assigned as string "${stringMatch[1]}" instead of enum`,
27715
+ file: relPath,
27716
+ line: i + 1,
27717
+ suggestion: `Use Action = PermissionAction.${stringMatch[1]} (typed enum)`
27718
+ });
27719
+ }
27720
+ }
27721
+ const typeSafeMatches = line.match(/PermissionAction\.\w+/g);
27722
+ if (typeSafeMatches) {
27723
+ for (const m of typeSafeMatches) {
27724
+ const value = m.replace("PermissionAction.", "");
27725
+ if (VALID_PERMISSION_ACTIONS.includes(value)) {
27726
+ typeSafeCount++;
27727
+ } else {
27728
+ result.errors.push({
27729
+ type: "error",
27730
+ category: "permissions",
27731
+ message: `PermissionAction.${value} is not a valid enum value`,
27732
+ file: relPath,
27733
+ line: i + 1,
27734
+ suggestion: `Valid PermissionAction values: ${VALID_PERMISSION_ACTIONS.join(", ")}`
27735
+ });
27736
+ }
27737
+ }
27738
+ }
27739
+ }
27740
+ }
27741
+ }
27742
+ if (enumParseCount > 0 || stringActionCount > 0 || typeSafeCount > 0) {
27743
+ result.warnings.push({
27744
+ type: "warning",
27745
+ category: "permissions",
27746
+ message: `Permission seeding summary: ${typeSafeCount} type-safe usages, ${enumParseCount} Enum.Parse anti-patterns, ${stringActionCount} string-based actions`
27747
+ });
27748
+ }
27749
+ if (enumParseCount > 0) {
27750
+ result.errors.push({
27751
+ type: "error",
27752
+ category: "permissions",
27753
+ message: `Found ${enumParseCount} Enum.Parse<PermissionAction> usage(s) \u2014 these cause runtime crashes if the string is invalid`,
27754
+ suggestion: 'Replace ALL Enum.Parse<PermissionAction>("...") with PermissionAction.{Value} for compile-time safety'
27755
+ });
27756
+ }
27757
+ }
27758
+ async function validateFrontendRoutes(structure, _config, result) {
27759
+ if (!structure.web) {
27760
+ result.warnings.push({
27761
+ type: "warning",
27762
+ category: "frontend-routes",
27763
+ message: "Web project not found, skipping frontend route validation"
27764
+ });
27765
+ return;
27766
+ }
27767
+ const appFiles = await findFiles("**/App.tsx", { cwd: structure.web });
27768
+ if (appFiles.length === 0) {
27769
+ result.warnings.push({
27770
+ type: "warning",
27771
+ category: "frontend-routes",
27772
+ message: "App.tsx not found in web project"
27773
+ });
27774
+ return;
27775
+ }
27776
+ const appContent = await readText(appFiles[0]);
27777
+ const hasClientRoutes = appContent.includes("clientRoutes");
27778
+ const hasRouteComponents = /<Route\s/.test(appContent);
27779
+ if (!hasClientRoutes && !hasRouteComponents) {
27780
+ result.errors.push({
27781
+ type: "error",
27782
+ category: "frontend-routes",
27783
+ message: "App.tsx has no route definitions (neither clientRoutes import nor <Route> components)",
27784
+ file: path8.relative(structure.root, appFiles[0]),
27785
+ suggestion: "Wire generated routes to App.tsx. Import clientRoutes from the generated file and render them."
27786
+ });
27787
+ return;
27788
+ }
27789
+ const seedFiles = await findFiles("**/*NavigationSeedData.cs", { cwd: structure.root });
27790
+ const seedNavRoutes = [];
27791
+ for (const file of seedFiles) {
27792
+ const content = await readText(file);
27793
+ const routeMatches = content.matchAll(/(?:route:\s*|Route\s*=\s*)["']([^"']+)["']/g);
27794
+ for (const match2 of routeMatches) {
27795
+ seedNavRoutes.push({
27796
+ route: match2[1],
27797
+ file: path8.relative(structure.root, file)
27798
+ });
27799
+ }
27800
+ }
27801
+ if (seedNavRoutes.length === 0) return;
27802
+ const missingRoutes = [];
27803
+ for (const nav of seedNavRoutes) {
27804
+ const routeSegments = nav.route.split("/").filter(Boolean);
27805
+ const lastSegment = routeSegments[routeSegments.length - 1];
27806
+ const moduleSegment = routeSegments.length > 1 ? routeSegments[routeSegments.length - 2] : "";
27807
+ const routeInApp = appContent.includes(nav.route) || appContent.includes(`path: '${nav.route}'`) || appContent.includes(`path="${nav.route}"`) || appContent.includes(`path: '/${lastSegment}'`) || appContent.includes(`path="/${lastSegment}"`) || moduleSegment && appContent.includes(`/${moduleSegment}/${lastSegment}`);
27808
+ if (!routeInApp) {
27809
+ missingRoutes.push(nav);
27810
+ }
27811
+ }
27812
+ if (missingRoutes.length > 0) {
27813
+ for (const missing of missingRoutes.slice(0, 5)) {
27814
+ result.errors.push({
27815
+ type: "error",
27816
+ category: "frontend-routes",
27817
+ message: `Navigation route "${missing.route}" from seed data not found in App.tsx`,
27818
+ file: path8.relative(structure.root, appFiles[0]),
27819
+ suggestion: `Add route for "${missing.route}" in App.tsx (defined in ${missing.file})`
27820
+ });
27821
+ }
27822
+ if (missingRoutes.length > 5) {
27823
+ result.errors.push({
27824
+ type: "error",
27825
+ category: "frontend-routes",
27826
+ message: `... and ${missingRoutes.length - 5} more seed data routes missing from App.tsx`
27827
+ });
27828
+ }
27829
+ }
27830
+ result.warnings.push({
27831
+ type: "warning",
27832
+ category: "frontend-routes",
27833
+ message: `Frontend routes summary: ${seedNavRoutes.length} seed data routes, ${missingRoutes.length} missing from App.tsx`
27834
+ });
27835
+ }
27836
+ async function validateFeatureJson(structure, _config, result) {
27837
+ const featureFiles = await findFiles("**/business-analyse/**/feature.json", { cwd: structure.root });
27838
+ if (featureFiles.length === 0) {
27839
+ result.warnings.push({
27840
+ type: "warning",
27841
+ category: "feature-json",
27842
+ message: "No feature.json files found, skipping feature.json validation"
27843
+ });
27844
+ return;
27845
+ }
27846
+ let appLevelFeature = null;
27847
+ let appLevelPath = "";
27848
+ for (const file of featureFiles) {
27849
+ const relPath = path8.relative(structure.root, file);
27850
+ let data;
27851
+ try {
27852
+ data = await readJson(file);
27853
+ } catch {
27854
+ result.errors.push({
27855
+ type: "error",
27856
+ category: "feature-json",
27857
+ message: `Invalid JSON in feature.json`,
27858
+ file: relPath,
27859
+ suggestion: "Fix the JSON syntax in this file"
27860
+ });
27861
+ continue;
27862
+ }
27863
+ if (data.scope === "application" || data.metadata?.scope === "application") {
27864
+ appLevelFeature = data;
27865
+ appLevelPath = relPath;
27866
+ }
27867
+ if (data.$schema && data.$schema.startsWith("http")) {
27868
+ result.errors.push({
27869
+ type: "error",
27870
+ category: "feature-json",
27871
+ message: `feature.json uses remote $schema URL instead of relative path`,
27872
+ file: relPath,
27873
+ suggestion: 'Use relative $schema path: "../../../schemas/feature-schema.json"'
27874
+ });
27875
+ }
27876
+ if (data.metadata?.language && data.metadata.language !== "fr") {
27877
+ result.warnings.push({
27878
+ type: "warning",
27879
+ category: "feature-json",
27880
+ message: `feature.json language is "${data.metadata.language}" instead of "fr"`,
27881
+ file: relPath,
27882
+ suggestion: 'SmartStack projects should use language: "fr" for consistency'
27883
+ });
27884
+ }
27885
+ const currentYear = (/* @__PURE__ */ new Date()).getFullYear().toString();
27886
+ const dates = [data.metadata?.createdAt, data.metadata?.updatedAt, data.metadata?.lastModified].filter(Boolean);
27887
+ for (const dateStr of dates) {
27888
+ if (dateStr && !dateStr.startsWith(currentYear) && !dateStr.startsWith((parseInt(currentYear) - 1).toString())) {
27889
+ result.warnings.push({
27890
+ type: "warning",
27891
+ category: "feature-json",
27892
+ message: `feature.json has date "${dateStr}" which doesn't match current year (${currentYear})`,
27893
+ file: relPath,
27894
+ suggestion: "Verify the date is correct \u2014 it may indicate a copy-paste error"
27895
+ });
27896
+ }
27897
+ }
27898
+ if (data.metadata?.lastModified && !data.metadata?.updatedAt) {
27899
+ result.errors.push({
27900
+ type: "error",
27901
+ category: "feature-json",
27902
+ message: 'feature.json uses "lastModified" instead of "updatedAt"',
27903
+ file: relPath,
27904
+ suggestion: 'Rename "lastModified" to "updatedAt" for schema compliance'
27905
+ });
27906
+ }
27907
+ if (data.metadata?.context && data.metadata.context !== "business" && relPath.includes("Business")) {
27908
+ result.warnings.push({
27909
+ type: "warning",
27910
+ category: "feature-json",
27911
+ message: `feature.json context is "${data.metadata.context}" but file is in Business directory`,
27912
+ file: relPath,
27913
+ suggestion: 'Use context: "business" for business domain features'
27914
+ });
27915
+ }
27916
+ if (data.metadata?.analysisMode && data.metadata.analysisMode !== "interactive") {
27917
+ result.errors.push({
27918
+ type: "error",
27919
+ category: "feature-json",
27920
+ message: `feature.json analysisMode is "${data.metadata.analysisMode}" instead of "interactive"`,
27921
+ file: relPath,
27922
+ suggestion: 'analysisMode must always be "interactive" (vibe_coding bypass was removed)'
27923
+ });
27924
+ }
27925
+ if (data.metadata?.tablePrefix) {
27926
+ if (!/^[a-z]{2,5}_$/.test(data.metadata.tablePrefix)) {
27927
+ result.errors.push({
27928
+ type: "error",
27929
+ category: "feature-json",
27930
+ message: `Invalid tablePrefix "${data.metadata.tablePrefix}"`,
27931
+ file: relPath,
27932
+ suggestion: "tablePrefix must match pattern ^[a-z]{2,5}_$ (e.g., rh_, fi_, crm_)"
27933
+ });
27934
+ }
27935
+ }
27936
+ if (!data.id) {
27937
+ result.errors.push({
27938
+ type: "error",
27939
+ category: "feature-json",
27940
+ message: 'feature.json missing required field "id"',
27941
+ file: relPath,
27942
+ suggestion: "Add id field with format FEAT-XXX"
27943
+ });
27944
+ } else if (!/^FEAT-\d{3,}$/.test(data.id)) {
27945
+ result.errors.push({
27946
+ type: "error",
27947
+ category: "feature-json",
27948
+ message: `feature.json id "${data.id}" does not match pattern FEAT-XXX`,
27949
+ file: relPath,
27950
+ suggestion: "Use format: FEAT-001, FEAT-002, etc."
27951
+ });
27952
+ }
27953
+ }
27954
+ if (appLevelFeature && featureFiles.length > 1) {
27955
+ for (const file of featureFiles) {
27956
+ const relPath = path8.relative(structure.root, file);
27957
+ if (relPath === appLevelPath) continue;
27958
+ try {
27959
+ const moduleData = await readJson(file);
27960
+ if (!moduleData.metadata) continue;
27961
+ if (appLevelFeature.metadata?.tablePrefix && moduleData.metadata.tablePrefix && moduleData.metadata.tablePrefix !== appLevelFeature.metadata.tablePrefix) {
27962
+ result.errors.push({
27963
+ type: "error",
27964
+ category: "feature-json",
27965
+ message: `Module tablePrefix "${moduleData.metadata.tablePrefix}" differs from app-level "${appLevelFeature.metadata.tablePrefix}"`,
27966
+ file: relPath,
27967
+ suggestion: `Use tablePrefix: "${appLevelFeature.metadata.tablePrefix}" to match the application-level feature.json`
27968
+ });
27969
+ }
27970
+ if (appLevelFeature.metadata?.language && moduleData.metadata.language && moduleData.metadata.language !== appLevelFeature.metadata.language) {
27971
+ result.warnings.push({
27972
+ type: "warning",
27973
+ category: "feature-json",
27974
+ message: `Module language "${moduleData.metadata.language}" differs from app-level "${appLevelFeature.metadata.language}"`,
27975
+ file: relPath,
27976
+ suggestion: `Use language: "${appLevelFeature.metadata.language}" for consistency`
27977
+ });
27978
+ }
27979
+ } catch {
27980
+ }
27981
+ }
27982
+ }
27983
+ result.warnings.push({
27984
+ type: "warning",
27985
+ category: "feature-json",
27986
+ message: `Feature.json summary: ${featureFiles.length} file(s) validated`
27987
+ });
27988
+ }
27556
27989
  function generateSummary(result, checks) {
27557
27990
  const parts = [];
27558
27991
  parts.push(`Checks performed: ${checks.join(", ")}`);
@@ -27598,7 +28031,7 @@ function formatResult(result) {
27598
28031
  }
27599
28032
  return lines.join("\n");
27600
28033
  }
27601
- var validateConventionsTool;
28034
+ var validateConventionsTool, VALID_PERMISSION_ACTIONS;
27602
28035
  var init_validate_conventions = __esm({
27603
28036
  "src/mcp/tools/validate-conventions.ts"() {
27604
28037
  "use strict";
@@ -27609,7 +28042,7 @@ var init_validate_conventions = __esm({
27609
28042
  init_logger();
27610
28043
  validateConventionsTool = {
27611
28044
  name: "validate_conventions",
27612
- description: "Validate AtlasHub/SmartStack conventions: SQL schemas (core/extensions), domain table prefixes (auth_, nav_, ai_, etc.), migration naming ({context}_v{version}_{sequence}_*), service interfaces (I*Service), namespace structure, controller routes (NavRoute)",
28045
+ description: "Validate AtlasHub/SmartStack conventions: SQL schemas (core/extensions), domain table prefixes (auth_, nav_, ai_, etc.), migration naming ({context}_v{version}_{sequence}_*), service interfaces (I*Service), namespace structure, controller routes (NavRoute), permission seeding safety",
27613
28046
  inputSchema: {
27614
28047
  type: "object",
27615
28048
  properties: {
@@ -27621,7 +28054,7 @@ var init_validate_conventions = __esm({
27621
28054
  type: "array",
27622
28055
  items: {
27623
28056
  type: "string",
27624
- enum: ["tables", "migrations", "services", "namespaces", "entities", "tenants", "controllers", "layouts", "tabs", "hierarchies", "protected-actions", "all"]
28057
+ enum: ["tables", "migrations", "services", "namespaces", "entities", "tenants", "controllers", "layouts", "tabs", "hierarchies", "protected-actions", "permissions", "frontend-routes", "feature-json", "all"]
27625
28058
  },
27626
28059
  description: "Types of checks to perform",
27627
28060
  default: ["all"]
@@ -27629,6 +28062,19 @@ var init_validate_conventions = __esm({
27629
28062
  }
27630
28063
  }
27631
28064
  };
28065
+ VALID_PERMISSION_ACTIONS = [
28066
+ "Access",
28067
+ "Read",
28068
+ "Create",
28069
+ "Update",
28070
+ "Delete",
28071
+ "Export",
28072
+ "Import",
28073
+ "Approve",
28074
+ "Reject",
28075
+ "Assign",
28076
+ "Execute"
28077
+ ];
27632
28078
  }
27633
28079
  });
27634
28080
 
@@ -34669,7 +35115,10 @@ async function scaffoldController(name, options, structure, config2, result, dry
34669
35115
  const namespace = options?.namespace || (hierarchy.controllerArea ? `${config2.conventions.namespaces.api}.Controllers.${hierarchy.controllerArea}` : `${config2.conventions.namespaces.api}.Controllers`);
34670
35116
  const navRoute = options?.navRoute;
34671
35117
  const navRouteSuffix = options?.navRouteSuffix;
34672
- const routeAttribute = navRoute ? navRouteSuffix ? `[NavRoute("${navRoute}", Suffix = "${navRouteSuffix}")]` : `[NavRoute("${navRoute}")]` : `[Route("api/[controller]")]`;
35118
+ const apiRoute = navRoute ? `api/${navRoute.replace(/\./g, "/")}${navRouteSuffix ? `/${navRouteSuffix}` : ""}` : null;
35119
+ const routeAttribute = navRoute ? navRouteSuffix ? `[Route("${apiRoute}")]
35120
+ [NavRoute("${navRoute}", Suffix = "${navRouteSuffix}")]` : `[Route("${apiRoute}")]
35121
+ [NavRoute("${navRoute}")]` : `[Route("api/[controller]")]`;
34673
35122
  const navRouteUsing = navRoute ? "using SmartStack.Api.Core.Routing;\n" : "";
34674
35123
  const controllerTemplate = `using Microsoft.AspNetCore.Authorization;
34675
35124
  using Microsoft.AspNetCore.Mvc;
@@ -34771,10 +35220,12 @@ public record Update{{name}}Request();
34771
35220
  }
34772
35221
  result.files.push({ path: controllerFilePath, content: controllerContent, type: "created" });
34773
35222
  if (navRoute) {
34774
- result.instructions.push("Controller created with NavRoute (Navigation-based routing).");
35223
+ result.instructions.push("Controller created with NavRoute + explicit Route (deterministic routing).");
34775
35224
  result.instructions.push(`NavRoute: ${navRoute}${navRouteSuffix ? ` (Suffix: ${navRouteSuffix})` : ""}`);
35225
+ result.instructions.push(`API Route: ${apiRoute}`);
34776
35226
  result.instructions.push("");
34777
- result.instructions.push("The actual API route will be resolved from Navigation entities at startup.");
35227
+ result.instructions.push("The [Route] attribute ensures deterministic API routing.");
35228
+ result.instructions.push("The [NavRoute] attribute integrates with the navigation/permission system.");
34778
35229
  result.instructions.push("Ensure the navigation path exists in the database:");
34779
35230
  result.instructions.push(` Context > Application > Module > Section matching "${navRoute}"`);
34780
35231
  } else {
@@ -52147,6 +52598,14 @@ function generatePermissionsForNavRoute(navRoute, customActions, includeStandard
52147
52598
  category: context
52148
52599
  });
52149
52600
  }
52601
+ for (const customAction of customActions) {
52602
+ if (!VALID_PERMISSION_ACTIONS2[customAction.toLowerCase()]) {
52603
+ const validActions = Object.keys(VALID_PERMISSION_ACTIONS2).join(", ");
52604
+ throw new Error(
52605
+ `Invalid custom action: "${customAction}". Valid PermissionAction values: ${validActions}.`
52606
+ );
52607
+ }
52608
+ }
52150
52609
  const actions = includeStandardActions ? [...STANDARD_ACTIONS, ...customActions] : customActions;
52151
52610
  for (const action of actions) {
52152
52611
  const code = `${navRoute}.${action}`;
@@ -52248,14 +52707,24 @@ function formatPermissionDescription(navRoute, action) {
52248
52707
  }
52249
52708
  function getActionVerb(action) {
52250
52709
  const verbs = {
52710
+ "access": "Access",
52251
52711
  "read": "View",
52252
52712
  "create": "Create",
52253
52713
  "update": "Update",
52254
52714
  "delete": "Delete",
52255
52715
  "export": "Export",
52256
- "import": "Import"
52716
+ "import": "Import",
52717
+ "approve": "Approve",
52718
+ "reject": "Reject",
52719
+ "assign": "Assign",
52720
+ "execute": "Execute"
52257
52721
  };
52258
- return verbs[action] || action.charAt(0).toUpperCase() + action.slice(1);
52722
+ const verb = verbs[action.toLowerCase()];
52723
+ if (!verb) {
52724
+ logger.warn(`Unknown permission action verb: "${action}". This may cause runtime errors.`);
52725
+ return action.charAt(0).toUpperCase() + action.slice(1);
52726
+ }
52727
+ return verb;
52259
52728
  }
52260
52729
  function formatPermissionsReport(permissions) {
52261
52730
  let report = "";
@@ -52324,17 +52793,14 @@ function generatePlaceholderGuid() {
52324
52793
  return "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX";
52325
52794
  }
52326
52795
  function getActionEnum(action) {
52327
- const actionMap = {
52328
- "read": "Read",
52329
- "create": "Create",
52330
- "update": "Update",
52331
- "delete": "Delete",
52332
- "assign": "Assign",
52333
- "execute": "Execute",
52334
- "export": "Export",
52335
- "import": "Import"
52336
- };
52337
- return actionMap[action.toLowerCase()] || action.charAt(0).toUpperCase() + action.slice(1);
52796
+ const result = VALID_PERMISSION_ACTIONS2[action.toLowerCase()];
52797
+ if (!result) {
52798
+ const validActions = Object.keys(VALID_PERMISSION_ACTIONS2).join(", ");
52799
+ throw new Error(
52800
+ `Invalid PermissionAction: "${action}". Valid actions: ${validActions}. Do NOT use Enum.Parse<PermissionAction>() with arbitrary strings \u2014 use typed enum values directly.`
52801
+ );
52802
+ }
52803
+ return result;
52338
52804
  }
52339
52805
  async function readDirectoryRecursive(dir) {
52340
52806
  const files = [];
@@ -52352,7 +52818,7 @@ async function readDirectoryRecursive(dir) {
52352
52818
  }
52353
52819
  return files;
52354
52820
  }
52355
- var HTTP_METHOD_TO_ACTION, STANDARD_ACTIONS, generatePermissionsTool;
52821
+ var HTTP_METHOD_TO_ACTION, STANDARD_ACTIONS, generatePermissionsTool, VALID_PERMISSION_ACTIONS2;
52356
52822
  var init_generate_permissions = __esm({
52357
52823
  "src/mcp/tools/generate-permissions.ts"() {
52358
52824
  "use strict";
@@ -52415,6 +52881,30 @@ After adding to PermissionConfiguration.cs, run: dotnet ef migrations add <Migra
52415
52881
  }
52416
52882
  }
52417
52883
  };
52884
+ VALID_PERMISSION_ACTIONS2 = {
52885
+ "access": "Access",
52886
+ // 0 - Wildcard permissions only
52887
+ "read": "Read",
52888
+ // 1
52889
+ "create": "Create",
52890
+ // 2
52891
+ "update": "Update",
52892
+ // 3
52893
+ "delete": "Delete",
52894
+ // 4
52895
+ "export": "Export",
52896
+ // 5
52897
+ "import": "Import",
52898
+ // 6
52899
+ "approve": "Approve",
52900
+ // 7
52901
+ "reject": "Reject",
52902
+ // 8
52903
+ "assign": "Assign",
52904
+ // 9
52905
+ "execute": "Execute"
52906
+ // 10
52907
+ };
52418
52908
  }
52419
52909
  });
52420
52910
 
@@ -56516,12 +57006,23 @@ async function scaffoldRoutes(input, config2) {
56516
57006
  const structure = await findSmartStackStructure(projectRoot);
56517
57007
  const webPath = structure.web || path19.join(projectRoot, "web");
56518
57008
  const routesPath = options?.outputPath || path19.join(webPath, "src", "routes");
56519
- const navRoutes = await discoverNavRoutes(structure, scope);
57009
+ const routeWarnings = [];
57010
+ const navRoutes = await discoverNavRoutes(structure, scope, routeWarnings);
56520
57011
  if (navRoutes.length === 0) {
56521
57012
  result.success = false;
56522
57013
  result.instructions.push("No NavRoute attributes found in controllers");
56523
57014
  return result;
56524
57015
  }
57016
+ if (routeWarnings.length > 0) {
57017
+ result.instructions.push("");
57018
+ result.instructions.push("### Route/NavRoute Mismatches Detected:");
57019
+ for (const warning of routeWarnings) {
57020
+ result.instructions.push(warning);
57021
+ }
57022
+ result.instructions.push("");
57023
+ result.instructions.push('Fix: Update [Route] attributes to match the NavRoute convention: api/{navRoute.replace(".", "/")}');
57024
+ result.instructions.push("");
57025
+ }
56525
57026
  if (generateRegistry) {
56526
57027
  const registryContent = generateNavRouteRegistry(navRoutes);
56527
57028
  const registryFile = path19.join(routesPath, "navRoutes.generated.ts");
@@ -56602,7 +57103,7 @@ async function scaffoldRoutes(input, config2) {
56602
57103
  }
56603
57104
  return result;
56604
57105
  }
56605
- async function discoverNavRoutes(structure, scope) {
57106
+ async function discoverNavRoutes(structure, scope, warnings) {
56606
57107
  const routes = [];
56607
57108
  const apiPaths = [];
56608
57109
  if (scope === "all" || scope === "platform") {
@@ -56659,6 +57160,7 @@ async function discoverNavRoutes(structure, scope) {
56659
57160
  permissions.push(match2[1]);
56660
57161
  }
56661
57162
  const fullNavRoute = suffix ? `${navRoute}.${suffix}` : navRoute;
57163
+ const expectedRoute = `api/${navRoute.replace(/\./g, "/")}${suffix ? `/${suffix}` : ""}`;
56662
57164
  routes.push({
56663
57165
  navRoute: fullNavRoute,
56664
57166
  apiPath: `/api/${navRoute.replace(/\./g, "/")}${suffix ? `/${suffix}` : ""}`,
@@ -56667,6 +57169,15 @@ async function discoverNavRoutes(structure, scope) {
56667
57169
  controller: controllerName,
56668
57170
  methods
56669
57171
  });
57172
+ const routeAttrMatch = content.match(/\[Route\s*\(\s*"([^"]+)"\s*\)\]/);
57173
+ if (routeAttrMatch && warnings) {
57174
+ const actualRoute = routeAttrMatch[1];
57175
+ if (actualRoute !== expectedRoute && actualRoute !== "api/[controller]") {
57176
+ warnings.push(
57177
+ `WARNING: ${controllerName}Controller has [Route("${actualRoute}")] that doesn't match NavRoute "${navRoute}". Expected [Route("${expectedRoute}")]`
57178
+ );
57179
+ }
57180
+ }
56670
57181
  }
56671
57182
  } catch {
56672
57183
  logger.debug(`Failed to parse controller: ${file}`);
@@ -57245,10 +57756,10 @@ import path20 from "path";
57245
57756
  async function handleValidateFrontendRoutes(args, config2) {
57246
57757
  const input = ValidateFrontendRoutesInputSchema.parse(args);
57247
57758
  logger.info("Validating frontend routes", { scope: input.scope });
57248
- const result = await validateFrontendRoutes(input, config2);
57759
+ const result = await validateFrontendRoutes2(input, config2);
57249
57760
  return formatResult6(result, input);
57250
57761
  }
57251
- async function validateFrontendRoutes(input, config2) {
57762
+ async function validateFrontendRoutes2(input, config2) {
57252
57763
  const result = {
57253
57764
  valid: true,
57254
57765
  registry: {
@@ -57480,6 +57991,21 @@ async function validateAppWiring(webPath, backendRoutes, result) {
57480
57991
  break;
57481
57992
  }
57482
57993
  }
57994
+ if (!appFilePath) {
57995
+ try {
57996
+ const deepSearch = await glob("**/App.tsx", {
57997
+ cwd: webPath,
57998
+ absolute: true,
57999
+ ignore: ["**/node_modules/**", "**/dist/**", "**/build/**"]
58000
+ });
58001
+ if (deepSearch.length > 0) {
58002
+ appFilePath = deepSearch[0];
58003
+ appContent = await readText(appFilePath);
58004
+ }
58005
+ } catch {
58006
+ logger.debug("Fallback App.tsx search failed");
58007
+ }
58008
+ }
57483
58009
  result.appWiring = {
57484
58010
  exists: !!appFilePath,
57485
58011
  routesImported: false,
@@ -57487,7 +58013,10 @@ async function validateAppWiring(webPath, backendRoutes, result) {
57487
58013
  issues: []
57488
58014
  };
57489
58015
  if (!appFilePath || !appContent) {
57490
- result.appWiring.issues.push("No App.tsx or main.tsx found in web/src/");
58016
+ const searchedPaths = appCandidates.map((c) => path20.join(webPath, "src", c)).join(", ");
58017
+ result.appWiring.issues.push(
58018
+ `No App.tsx or main.tsx found. Searched: ${searchedPaths} and ${webPath}/**/App.tsx`
58019
+ );
57491
58020
  return;
57492
58021
  }
57493
58022
  const hasClientRoutesImport = appContent.includes("clientRoutes.generated");
@@ -59528,7 +60057,8 @@ async function handleValidateSecurity(args, config2) {
59528
60057
  "tenant-isolation",
59529
60058
  "authorization",
59530
60059
  "dangerous-functions",
59531
- "input-validation"
60060
+ "input-validation",
60061
+ "guid-empty"
59532
60062
  ] : input.checks;
59533
60063
  logger.info("Validating security", { projectPath, checks: checksToRun });
59534
60064
  const result = {
@@ -59561,6 +60091,9 @@ async function handleValidateSecurity(args, config2) {
59561
60091
  if (checksToRun.includes("input-validation")) {
59562
60092
  await checkInputValidation(structure, result);
59563
60093
  }
60094
+ if (checksToRun.includes("guid-empty")) {
60095
+ await checkGuidEmpty(structure, result);
60096
+ }
59564
60097
  result.stats.blocking = result.findings.filter((f) => f.severity === "blocking").length;
59565
60098
  result.stats.critical = result.findings.filter((f) => f.severity === "critical").length;
59566
60099
  result.stats.warning = result.findings.filter((f) => f.severity === "warning").length;
@@ -59688,22 +60221,60 @@ async function checkTenantIsolation(structure, result) {
59688
60221
  const serviceFiles = await findFiles("**/*Service.cs", { cwd: structure.application });
59689
60222
  for (const file of serviceFiles) {
59690
60223
  const content = await readText(file);
59691
- const directAccessPattern = /_context\.\w+\.(?:First|Single|Find|ToList)\s*\(/g;
60224
+ const lines = content.split("\n");
60225
+ const tenantDbSets = /* @__PURE__ */ new Set();
60226
+ for (const entity of tenantEntities) {
60227
+ tenantDbSets.add(entity + "s");
60228
+ tenantDbSets.add(entity + "es");
60229
+ tenantDbSets.add(entity + "ies");
60230
+ tenantDbSets.add(entity);
60231
+ }
60232
+ const dbSetAccessPattern = /_context\.(\w+)/g;
59692
60233
  let match2;
59693
- while ((match2 = directAccessPattern.exec(content)) !== null) {
59694
- const surroundingCode = content.substring(Math.max(0, match2.index - 100), match2.index + 100);
59695
- if (!surroundingCode.includes(".Where(") && !surroundingCode.includes("TenantId")) {
59696
- const lineNumber = getLineNumber(content, match2.index);
59697
- result.findings.push({
59698
- severity: "critical",
59699
- category: "tenant-isolation",
59700
- message: "Direct DbContext access without tenant filtering",
59701
- file: path23.relative(structure.root, file),
59702
- line: lineNumber,
59703
- code: truncateCode(match2[0]),
59704
- suggestion: "Use repository with global tenant filter or add explicit TenantId filter",
59705
- cweId: "CWE-639"
59706
- });
60234
+ while ((match2 = dbSetAccessPattern.exec(content)) !== null) {
60235
+ const dbSetName = match2[1];
60236
+ if (tenantEntities.length > 0 && !tenantDbSets.has(dbSetName)) {
60237
+ continue;
60238
+ }
60239
+ const chainStart = match2.index;
60240
+ const semicolonIndex = content.indexOf(";", chainStart);
60241
+ if (semicolonIndex === -1) continue;
60242
+ const fullChain = content.substring(chainStart, semicolonIndex);
60243
+ if (fullChain.includes("TenantId")) continue;
60244
+ if (/\.Add\s*\(/.test(fullChain) || /\.Remove\s*\(/.test(fullChain) || /\.Update\s*\(/.test(fullChain)) continue;
60245
+ if (!fullChain.includes(".") || !fullChain.includes("(")) continue;
60246
+ const lineNumber = getLineNumber(content, match2.index);
60247
+ const lineContent = lines[lineNumber - 1]?.trim() || "";
60248
+ result.findings.push({
60249
+ severity: "blocking",
60250
+ category: "tenant-isolation",
60251
+ message: `Query on _context.${dbSetName} without TenantId filter`,
60252
+ file: path23.relative(structure.root, file),
60253
+ line: lineNumber,
60254
+ code: truncateCode(lineContent),
60255
+ suggestion: "Add .Where(x => x.TenantId == tenantId) to the query chain, or ensure a global query filter is applied.",
60256
+ cweId: "CWE-639"
60257
+ });
60258
+ }
60259
+ for (const entity of tenantEntities) {
60260
+ const newEntityPattern = new RegExp(`new\\s+${entity}\\s*\\{([^}]*)\\}`, "gs");
60261
+ let newMatch;
60262
+ while ((newMatch = newEntityPattern.exec(content)) !== null) {
60263
+ const objectInitializer = newMatch[1];
60264
+ if (!objectInitializer.includes("TenantId")) {
60265
+ const lineNumber = getLineNumber(content, newMatch.index);
60266
+ const lineContent = lines[lineNumber - 1]?.trim() || "";
60267
+ result.findings.push({
60268
+ severity: "blocking",
60269
+ category: "tenant-isolation",
60270
+ message: `new ${entity} { } without TenantId assignment`,
60271
+ file: path23.relative(structure.root, file),
60272
+ line: lineNumber,
60273
+ code: truncateCode(lineContent),
60274
+ suggestion: `Add TenantId = tenantId to the object initializer, or use ${entity}.Create(tenantId, ...) factory method.`,
60275
+ cweId: "CWE-639"
60276
+ });
60277
+ }
59707
60278
  }
59708
60279
  }
59709
60280
  }
@@ -59829,6 +60400,49 @@ async function checkInputValidation(structure, result) {
59829
60400
  }
59830
60401
  }
59831
60402
  }
60403
+ async function checkGuidEmpty(structure, result) {
60404
+ const searchPaths = [];
60405
+ if (structure.application) searchPaths.push(structure.application);
60406
+ if (structure.api) searchPaths.push(structure.api);
60407
+ if (searchPaths.length === 0) return;
60408
+ const guidEmptyPattern = /Guid\.Empty/g;
60409
+ for (const searchPath of searchPaths) {
60410
+ const serviceFiles = await findFiles("**/*Service.cs", { cwd: searchPath });
60411
+ const controllerFiles = await findFiles("**/Controllers/**/*Controller.cs", { cwd: searchPath });
60412
+ const filesToScan = [...serviceFiles, ...controllerFiles];
60413
+ result.stats.filesScanned += filesToScan.length;
60414
+ for (const file of filesToScan) {
60415
+ if (isExcludedFile(file)) continue;
60416
+ const content = await readText(file);
60417
+ const lines = content.split("\n");
60418
+ guidEmptyPattern.lastIndex = 0;
60419
+ let match2;
60420
+ while ((match2 = guidEmptyPattern.exec(content)) !== null) {
60421
+ const lineNumber = getLineNumber(content, match2.index);
60422
+ const lineContent = lines[lineNumber - 1]?.trim() || "";
60423
+ if (lineContent.startsWith("//") || lineContent.startsWith("*") || lineContent.startsWith("/*")) {
60424
+ continue;
60425
+ }
60426
+ if (/[!=]=\s*Guid\.Empty/.test(lineContent) || /Guid\.Empty\s*[!=]=/.test(lineContent)) {
60427
+ continue;
60428
+ }
60429
+ if (/default\s*[:(]/.test(lineContent) || /\?\?\s*Guid\.Empty/.test(lineContent)) {
60430
+ continue;
60431
+ }
60432
+ result.findings.push({
60433
+ severity: "critical",
60434
+ category: "guid-empty",
60435
+ message: "Guid.Empty used as a value in business logic",
60436
+ file: path23.relative(structure.root, file),
60437
+ line: lineNumber,
60438
+ code: truncateCode(lineContent),
60439
+ suggestion: "Replace Guid.Empty with actual value from the current user context (e.g., currentUser.Id, tenantId). Guid.Empty typically indicates a missing resolution.",
60440
+ cweId: "CWE-639"
60441
+ });
60442
+ }
60443
+ }
60444
+ }
60445
+ }
59832
60446
  async function getFilesToScan(structure, patterns) {
59833
60447
  const allFiles = [];
59834
60448
  for (const pattern of patterns) {
@@ -59971,6 +60585,7 @@ var init_validate_security = __esm({
59971
60585
  "authorization",
59972
60586
  "dangerous-functions",
59973
60587
  "input-validation",
60588
+ "guid-empty",
59974
60589
  "xss",
59975
60590
  "csrf",
59976
60591
  "logging-sensitive",
@@ -61309,6 +61924,90 @@ async function checkSecurity(context) {
61309
61924
  }
61310
61925
  }
61311
61926
  }
61927
+ if (file.language === "csharp" && file.relativePath.includes("Service")) {
61928
+ const dbSetPattern = /_context\.(\w+)\./g;
61929
+ let dbSetMatch;
61930
+ dbSetPattern.lastIndex = 0;
61931
+ while ((dbSetMatch = dbSetPattern.exec(file.content)) !== null) {
61932
+ const chainStart = dbSetMatch.index;
61933
+ const semicolonIndex = file.content.indexOf(";", chainStart);
61934
+ if (semicolonIndex === -1) continue;
61935
+ const fullChain = file.content.substring(chainStart, semicolonIndex);
61936
+ if (/\.Add\s*\(/.test(fullChain) || /\.Remove\s*\(/.test(fullChain) || /\.Update\s*\(/.test(fullChain)) continue;
61937
+ if (!fullChain.includes("(")) continue;
61938
+ if (file.content.includes("ITenantEntity") || file.content.includes("TenantId")) {
61939
+ if (!fullChain.includes("TenantId")) {
61940
+ const lineNumber = getLineNumber3(file.content, dbSetMatch.index);
61941
+ const lineContent = lines[lineNumber - 1]?.trim() || "";
61942
+ if (isInComment(lineContent)) continue;
61943
+ findings.push({
61944
+ id: generateFindingId("security"),
61945
+ category: "security",
61946
+ severity: "blocking",
61947
+ title: "Query Without Tenant Filter",
61948
+ description: `Query on _context.${dbSetMatch[1]} does not include TenantId filter. In a multi-tenant application, all queries must filter by tenant.`,
61949
+ file: file.relativePath,
61950
+ line: lineNumber,
61951
+ code: truncateCode2(lineContent),
61952
+ suggestion: "Add .Where(x => x.TenantId == tenantId) or ensure a global query filter is applied.",
61953
+ autoFixable: false,
61954
+ cweId: "CWE-639"
61955
+ });
61956
+ }
61957
+ }
61958
+ }
61959
+ if (file.content.includes("ITenantEntity") || file.content.includes(": TenantEntity")) {
61960
+ const newEntityPattern = /new\s+(\w+)\s*\{([^}]*)\}/gs;
61961
+ let newMatch;
61962
+ newEntityPattern.lastIndex = 0;
61963
+ while ((newMatch = newEntityPattern.exec(file.content)) !== null) {
61964
+ const entityName = newMatch[1];
61965
+ const initializer3 = newMatch[2];
61966
+ if (/Dto|Request|Response|Result|Exception|Options|Config/i.test(entityName)) continue;
61967
+ if (!initializer3.includes("TenantId")) {
61968
+ const lineNumber = getLineNumber3(file.content, newMatch.index);
61969
+ const lineContent = lines[lineNumber - 1]?.trim() || "";
61970
+ if (isInComment(lineContent)) continue;
61971
+ findings.push({
61972
+ id: generateFindingId("security"),
61973
+ category: "security",
61974
+ severity: "critical",
61975
+ title: "Entity Creation Without TenantId",
61976
+ description: `new ${entityName} { } without TenantId assignment. Tenant entities must always have TenantId set.`,
61977
+ file: file.relativePath,
61978
+ line: lineNumber,
61979
+ code: truncateCode2(lineContent),
61980
+ suggestion: `Add TenantId = tenantId to the initializer, or use ${entityName}.Create(tenantId, ...).`,
61981
+ autoFixable: false,
61982
+ cweId: "CWE-639"
61983
+ });
61984
+ }
61985
+ }
61986
+ }
61987
+ const guidEmptyPattern = /Guid\.Empty/g;
61988
+ let guidMatch;
61989
+ guidEmptyPattern.lastIndex = 0;
61990
+ while ((guidMatch = guidEmptyPattern.exec(file.content)) !== null) {
61991
+ const lineNumber = getLineNumber3(file.content, guidMatch.index);
61992
+ const lineContent = lines[lineNumber - 1]?.trim() || "";
61993
+ if (isInComment(lineContent)) continue;
61994
+ if (/[!=]=\s*Guid\.Empty/.test(lineContent) || /Guid\.Empty\s*[!=]=/.test(lineContent)) continue;
61995
+ if (/default\s*[:(]/.test(lineContent) || /\?\?\s*Guid\.Empty/.test(lineContent)) continue;
61996
+ findings.push({
61997
+ id: generateFindingId("security"),
61998
+ category: "security",
61999
+ severity: "critical",
62000
+ title: "Guid.Empty Used as Value",
62001
+ description: "Guid.Empty used as a business value. This typically indicates a missing resolution (e.g., current user ID, tenant ID).",
62002
+ file: file.relativePath,
62003
+ line: lineNumber,
62004
+ code: truncateCode2(lineContent),
62005
+ suggestion: "Replace Guid.Empty with the actual value from user context (currentUser.Id, tenantId, etc.).",
62006
+ autoFixable: false,
62007
+ cweId: "CWE-639"
62008
+ });
62009
+ }
62010
+ }
61312
62011
  if (["typescript", "javascript", "tsx", "jsx"].includes(file.language)) {
61313
62012
  for (const { pattern, name } of XSS_PATTERNS) {
61314
62013
  let match2;