@atlashub/smartstack-mcp 1.14.0 → 1.15.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.
- package/dist/index.js +976 -20
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -82,10 +82,10 @@ import { stat, mkdir, readFile, writeFile, cp, rm } from "fs/promises";
|
|
|
82
82
|
import path from "path";
|
|
83
83
|
import { glob } from "glob";
|
|
84
84
|
var FileSystemError = class extends Error {
|
|
85
|
-
constructor(message, operation,
|
|
85
|
+
constructor(message, operation, path26, cause) {
|
|
86
86
|
super(message);
|
|
87
87
|
this.operation = operation;
|
|
88
|
-
this.path =
|
|
88
|
+
this.path = path26;
|
|
89
89
|
this.cause = cause;
|
|
90
90
|
this.name = "FileSystemError";
|
|
91
91
|
}
|
|
@@ -648,6 +648,39 @@ var SuggestTestScenariosInputSchema = z.object({
|
|
|
648
648
|
name: z.string().min(1).describe("Component name or file path"),
|
|
649
649
|
depth: z.enum(["basic", "comprehensive", "security-focused"]).default("comprehensive").describe("Depth of analysis")
|
|
650
650
|
});
|
|
651
|
+
var SecurityCheckSchema = z.enum([
|
|
652
|
+
"hardcoded-secrets",
|
|
653
|
+
"sql-injection",
|
|
654
|
+
"tenant-isolation",
|
|
655
|
+
"authorization",
|
|
656
|
+
"dangerous-functions",
|
|
657
|
+
"input-validation",
|
|
658
|
+
"xss",
|
|
659
|
+
"csrf",
|
|
660
|
+
"logging-sensitive",
|
|
661
|
+
"all"
|
|
662
|
+
]);
|
|
663
|
+
var ValidateSecurityInputSchema = z.object({
|
|
664
|
+
path: z.string().optional().describe("Project path to validate (default: SmartStack.app path)"),
|
|
665
|
+
checks: z.array(SecurityCheckSchema).default(["all"]).describe("Security checks to run"),
|
|
666
|
+
severity: z.enum(["blocking", "all"]).optional().describe("Filter results by severity")
|
|
667
|
+
});
|
|
668
|
+
var QualityMetricSchema = z.enum([
|
|
669
|
+
"cognitive-complexity",
|
|
670
|
+
"cyclomatic-complexity",
|
|
671
|
+
"function-size",
|
|
672
|
+
"nesting-depth",
|
|
673
|
+
"parameter-count",
|
|
674
|
+
"code-duplication",
|
|
675
|
+
"file-size",
|
|
676
|
+
"all"
|
|
677
|
+
]);
|
|
678
|
+
var AnalyzeCodeQualityInputSchema = z.object({
|
|
679
|
+
path: z.string().optional().describe("Project path to analyze (default: SmartStack.app path)"),
|
|
680
|
+
metrics: z.array(QualityMetricSchema).default(["all"]).describe("Metrics to analyze"),
|
|
681
|
+
threshold: z.enum(["strict", "normal", "lenient"]).default("normal").describe("Threshold level for violations"),
|
|
682
|
+
scope: z.enum(["changed", "all"]).default("all").describe("Analyze only changed files or all")
|
|
683
|
+
});
|
|
651
684
|
var ScaffoldApiClientInputSchema = z.object({
|
|
652
685
|
path: z.string().optional().describe("Path to SmartStack project root (defaults to configured project path)"),
|
|
653
686
|
navRoute: z.string().min(1).describe('NavRoute path (e.g., "platform.administration.users")'),
|
|
@@ -9735,6 +9768,919 @@ function generateRecommendations3(analyzed) {
|
|
|
9735
9768
|
return recommendations;
|
|
9736
9769
|
}
|
|
9737
9770
|
|
|
9771
|
+
// src/tools/validate-security.ts
|
|
9772
|
+
import path20 from "path";
|
|
9773
|
+
var validateSecurityTool = {
|
|
9774
|
+
name: "validate_security",
|
|
9775
|
+
description: "Validate SmartStack security patterns: multi-tenant isolation, authorization, input validation, secrets exposure, injection vulnerabilities, and OWASP Top 10 compliance.",
|
|
9776
|
+
inputSchema: {
|
|
9777
|
+
type: "object",
|
|
9778
|
+
properties: {
|
|
9779
|
+
path: {
|
|
9780
|
+
type: "string",
|
|
9781
|
+
description: "Project path to validate (default: SmartStack.app path from config)"
|
|
9782
|
+
},
|
|
9783
|
+
checks: {
|
|
9784
|
+
type: "array",
|
|
9785
|
+
items: {
|
|
9786
|
+
type: "string",
|
|
9787
|
+
enum: [
|
|
9788
|
+
"hardcoded-secrets",
|
|
9789
|
+
"sql-injection",
|
|
9790
|
+
"tenant-isolation",
|
|
9791
|
+
"authorization",
|
|
9792
|
+
"dangerous-functions",
|
|
9793
|
+
"input-validation",
|
|
9794
|
+
"xss",
|
|
9795
|
+
"csrf",
|
|
9796
|
+
"logging-sensitive",
|
|
9797
|
+
"all"
|
|
9798
|
+
]
|
|
9799
|
+
},
|
|
9800
|
+
description: "Security checks to run",
|
|
9801
|
+
default: ["all"]
|
|
9802
|
+
},
|
|
9803
|
+
severity: {
|
|
9804
|
+
type: "string",
|
|
9805
|
+
enum: ["blocking", "all"],
|
|
9806
|
+
description: "Filter results by severity"
|
|
9807
|
+
}
|
|
9808
|
+
}
|
|
9809
|
+
}
|
|
9810
|
+
};
|
|
9811
|
+
async function handleValidateSecurity(args, config) {
|
|
9812
|
+
const input = ValidateSecurityInputSchema.parse(args);
|
|
9813
|
+
const projectPath = input.path || config.smartstack.projectPath;
|
|
9814
|
+
const checksToRun = input.checks.includes("all") ? [
|
|
9815
|
+
"hardcoded-secrets",
|
|
9816
|
+
"sql-injection",
|
|
9817
|
+
"tenant-isolation",
|
|
9818
|
+
"authorization",
|
|
9819
|
+
"dangerous-functions",
|
|
9820
|
+
"input-validation"
|
|
9821
|
+
] : input.checks;
|
|
9822
|
+
logger.info("Validating security", { projectPath, checks: checksToRun });
|
|
9823
|
+
const result = {
|
|
9824
|
+
valid: true,
|
|
9825
|
+
summary: "",
|
|
9826
|
+
findings: [],
|
|
9827
|
+
stats: {
|
|
9828
|
+
blocking: 0,
|
|
9829
|
+
critical: 0,
|
|
9830
|
+
warning: 0,
|
|
9831
|
+
filesScanned: 0
|
|
9832
|
+
}
|
|
9833
|
+
};
|
|
9834
|
+
const structure = await findSmartStackStructure(projectPath);
|
|
9835
|
+
if (checksToRun.includes("hardcoded-secrets")) {
|
|
9836
|
+
await checkHardcodedSecrets(structure, result);
|
|
9837
|
+
}
|
|
9838
|
+
if (checksToRun.includes("sql-injection")) {
|
|
9839
|
+
await checkSqlInjection(structure, result);
|
|
9840
|
+
}
|
|
9841
|
+
if (checksToRun.includes("tenant-isolation")) {
|
|
9842
|
+
await checkTenantIsolation(structure, result);
|
|
9843
|
+
}
|
|
9844
|
+
if (checksToRun.includes("authorization")) {
|
|
9845
|
+
await checkAuthorization(structure, result);
|
|
9846
|
+
}
|
|
9847
|
+
if (checksToRun.includes("dangerous-functions")) {
|
|
9848
|
+
await checkDangerousFunctions(structure, result);
|
|
9849
|
+
}
|
|
9850
|
+
if (checksToRun.includes("input-validation")) {
|
|
9851
|
+
await checkInputValidation(structure, result);
|
|
9852
|
+
}
|
|
9853
|
+
result.stats.blocking = result.findings.filter((f) => f.severity === "blocking").length;
|
|
9854
|
+
result.stats.critical = result.findings.filter((f) => f.severity === "critical").length;
|
|
9855
|
+
result.stats.warning = result.findings.filter((f) => f.severity === "warning").length;
|
|
9856
|
+
if (input.severity === "blocking") {
|
|
9857
|
+
result.findings = result.findings.filter((f) => f.severity === "blocking");
|
|
9858
|
+
}
|
|
9859
|
+
result.valid = result.stats.blocking === 0;
|
|
9860
|
+
result.summary = result.valid ? `Security validation passed. ${result.stats.warning} warning(s) found.` : `Security validation FAILED. ${result.stats.blocking} blocking issue(s) found.`;
|
|
9861
|
+
return formatSecurityReport(result);
|
|
9862
|
+
}
|
|
9863
|
+
async function checkHardcodedSecrets(structure, result) {
|
|
9864
|
+
const secretPatterns = [
|
|
9865
|
+
// Credentials
|
|
9866
|
+
{ pattern: /(?:password|passwd|pwd)\s*[=:]\s*["'][^"']{4,}["']/gi, name: "password" },
|
|
9867
|
+
{ pattern: /(?:api[_-]?key|apikey)\s*[=:]\s*["'][^"']{8,}["']/gi, name: "API key" },
|
|
9868
|
+
{ pattern: /(?:secret|token)\s*[=:]\s*["'][^"']{8,}["']/gi, name: "secret/token" },
|
|
9869
|
+
// Connection strings with embedded passwords
|
|
9870
|
+
{ pattern: /Server=.+;.*Password=[^;]+/gi, name: "connection string with password" },
|
|
9871
|
+
{ pattern: /Data Source=.+;.*Password=[^;]+/gi, name: "connection string with password" },
|
|
9872
|
+
// Private keys
|
|
9873
|
+
{ pattern: /-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----/g, name: "private key" }
|
|
9874
|
+
];
|
|
9875
|
+
const filesToScan = await getFilesToScan(structure, ["**/*.cs", "**/*.ts", "**/*.tsx", "**/*.json"]);
|
|
9876
|
+
result.stats.filesScanned += filesToScan.length;
|
|
9877
|
+
for (const file of filesToScan) {
|
|
9878
|
+
if (isExcludedFile(file)) continue;
|
|
9879
|
+
const content = await readText(file);
|
|
9880
|
+
const lines = content.split("\n");
|
|
9881
|
+
for (const { pattern, name } of secretPatterns) {
|
|
9882
|
+
pattern.lastIndex = 0;
|
|
9883
|
+
let match;
|
|
9884
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
9885
|
+
const lineNumber = getLineNumber(content, match.index);
|
|
9886
|
+
const lineContent = lines[lineNumber - 1]?.trim() || "";
|
|
9887
|
+
if (isPlaceholderValue(lineContent)) continue;
|
|
9888
|
+
result.findings.push({
|
|
9889
|
+
severity: "blocking",
|
|
9890
|
+
category: "hardcoded-secrets",
|
|
9891
|
+
message: `Hardcoded ${name} detected`,
|
|
9892
|
+
file: path20.relative(structure.root, file),
|
|
9893
|
+
line: lineNumber,
|
|
9894
|
+
code: truncateCode(lineContent),
|
|
9895
|
+
suggestion: `Move ${name} to configuration or environment variables`,
|
|
9896
|
+
cweId: "CWE-798"
|
|
9897
|
+
});
|
|
9898
|
+
}
|
|
9899
|
+
}
|
|
9900
|
+
}
|
|
9901
|
+
}
|
|
9902
|
+
async function checkSqlInjection(structure, result) {
|
|
9903
|
+
const sqlInjectionPatterns = [
|
|
9904
|
+
// Raw SQL with string concatenation (the most dangerous pattern)
|
|
9905
|
+
{ pattern: /\.(?:FromSqlRaw|ExecuteSqlRaw)\s*\([^)]*\s*\+\s*/g, name: "concatenated SQL in FromSqlRaw/ExecuteSqlRaw" },
|
|
9906
|
+
// ADO.NET without parameters
|
|
9907
|
+
{ pattern: /new SqlCommand\s*\([^)]*\+/g, name: "concatenated SQL in SqlCommand" },
|
|
9908
|
+
{ pattern: /CommandText\s*=\s*[^;]*\+/g, name: "concatenated SQL in CommandText" }
|
|
9909
|
+
// Note: FromSqlInterpolated with $"" is safe and not flagged
|
|
9910
|
+
];
|
|
9911
|
+
const csFiles = await getFilesToScan(structure, ["**/*.cs"]);
|
|
9912
|
+
result.stats.filesScanned += csFiles.length;
|
|
9913
|
+
for (const file of csFiles) {
|
|
9914
|
+
if (isExcludedFile(file)) continue;
|
|
9915
|
+
const content = await readText(file);
|
|
9916
|
+
const lines = content.split("\n");
|
|
9917
|
+
for (const { pattern, name } of sqlInjectionPatterns) {
|
|
9918
|
+
pattern.lastIndex = 0;
|
|
9919
|
+
let match;
|
|
9920
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
9921
|
+
const lineNumber = getLineNumber(content, match.index);
|
|
9922
|
+
const lineContent = lines[lineNumber - 1]?.trim() || "";
|
|
9923
|
+
result.findings.push({
|
|
9924
|
+
severity: "blocking",
|
|
9925
|
+
category: "sql-injection",
|
|
9926
|
+
message: `Potential SQL injection: ${name}`,
|
|
9927
|
+
file: path20.relative(structure.root, file),
|
|
9928
|
+
line: lineNumber,
|
|
9929
|
+
code: truncateCode(lineContent),
|
|
9930
|
+
suggestion: 'Use parameterized queries: FromSqlRaw("SELECT * FROM x WHERE id = {0}", id) or FromSqlInterpolated',
|
|
9931
|
+
cweId: "CWE-89"
|
|
9932
|
+
});
|
|
9933
|
+
}
|
|
9934
|
+
}
|
|
9935
|
+
}
|
|
9936
|
+
}
|
|
9937
|
+
async function checkTenantIsolation(structure, result) {
|
|
9938
|
+
if (!structure.domain) {
|
|
9939
|
+
result.findings.push({
|
|
9940
|
+
severity: "warning",
|
|
9941
|
+
category: "tenant-isolation",
|
|
9942
|
+
message: "Domain project not found, skipping tenant isolation validation",
|
|
9943
|
+
file: "",
|
|
9944
|
+
suggestion: "Ensure project structure follows SmartStack conventions"
|
|
9945
|
+
});
|
|
9946
|
+
return;
|
|
9947
|
+
}
|
|
9948
|
+
const entityFiles = await findFiles("**/Entities/**/*.cs", { cwd: structure.domain });
|
|
9949
|
+
const tenantEntities = [];
|
|
9950
|
+
for (const file of entityFiles) {
|
|
9951
|
+
const content = await readText(file);
|
|
9952
|
+
if (content.includes("ITenantEntity") || content.includes(": TenantEntity")) {
|
|
9953
|
+
const entityMatch = content.match(/class\s+(\w+)/);
|
|
9954
|
+
if (entityMatch) {
|
|
9955
|
+
tenantEntities.push(entityMatch[1]);
|
|
9956
|
+
}
|
|
9957
|
+
}
|
|
9958
|
+
const createMethodMatches = content.matchAll(/public\s+static\s+\w+\s+Create\s*\(([^)]*)\)/g);
|
|
9959
|
+
for (const match of createMethodMatches) {
|
|
9960
|
+
const params = match[1];
|
|
9961
|
+
if ((content.includes("ITenantEntity") || content.includes(": TenantEntity")) && !params.includes("tenantId") && !params.includes("TenantId")) {
|
|
9962
|
+
const lineNumber = getLineNumber(content, match.index);
|
|
9963
|
+
result.findings.push({
|
|
9964
|
+
severity: "blocking",
|
|
9965
|
+
category: "tenant-isolation",
|
|
9966
|
+
message: "Create method missing tenantId parameter for tenant entity",
|
|
9967
|
+
file: path20.relative(structure.root, file),
|
|
9968
|
+
line: lineNumber,
|
|
9969
|
+
code: truncateCode(match[0]),
|
|
9970
|
+
suggestion: "Add Guid tenantId as first parameter: Create(Guid tenantId, ...)",
|
|
9971
|
+
cweId: "CWE-639"
|
|
9972
|
+
});
|
|
9973
|
+
}
|
|
9974
|
+
}
|
|
9975
|
+
}
|
|
9976
|
+
if (structure.application) {
|
|
9977
|
+
const serviceFiles = await findFiles("**/*Service.cs", { cwd: structure.application });
|
|
9978
|
+
for (const file of serviceFiles) {
|
|
9979
|
+
const content = await readText(file);
|
|
9980
|
+
const directAccessPattern = /_context\.\w+\.(?:First|Single|Find|ToList)\s*\(/g;
|
|
9981
|
+
let match;
|
|
9982
|
+
while ((match = directAccessPattern.exec(content)) !== null) {
|
|
9983
|
+
const surroundingCode = content.substring(Math.max(0, match.index - 100), match.index + 100);
|
|
9984
|
+
if (!surroundingCode.includes(".Where(") && !surroundingCode.includes("TenantId")) {
|
|
9985
|
+
const lineNumber = getLineNumber(content, match.index);
|
|
9986
|
+
result.findings.push({
|
|
9987
|
+
severity: "critical",
|
|
9988
|
+
category: "tenant-isolation",
|
|
9989
|
+
message: "Direct DbContext access without tenant filtering",
|
|
9990
|
+
file: path20.relative(structure.root, file),
|
|
9991
|
+
line: lineNumber,
|
|
9992
|
+
code: truncateCode(match[0]),
|
|
9993
|
+
suggestion: "Use repository with global tenant filter or add explicit TenantId filter",
|
|
9994
|
+
cweId: "CWE-639"
|
|
9995
|
+
});
|
|
9996
|
+
}
|
|
9997
|
+
}
|
|
9998
|
+
}
|
|
9999
|
+
}
|
|
10000
|
+
}
|
|
10001
|
+
async function checkAuthorization(structure, result) {
|
|
10002
|
+
const apiPath = structure.api || structure.apiCore;
|
|
10003
|
+
if (!apiPath) {
|
|
10004
|
+
result.findings.push({
|
|
10005
|
+
severity: "warning",
|
|
10006
|
+
category: "authorization",
|
|
10007
|
+
message: "API project not found, skipping authorization validation",
|
|
10008
|
+
file: "",
|
|
10009
|
+
suggestion: "Ensure project structure follows SmartStack conventions"
|
|
10010
|
+
});
|
|
10011
|
+
return;
|
|
10012
|
+
}
|
|
10013
|
+
const controllerFiles = await findFiles("**/Controllers/**/*.cs", { cwd: apiPath });
|
|
10014
|
+
result.stats.filesScanned += controllerFiles.length;
|
|
10015
|
+
for (const file of controllerFiles) {
|
|
10016
|
+
const content = await readText(file);
|
|
10017
|
+
if (content.includes("[ApiController]")) {
|
|
10018
|
+
const hasClassLevelAuth = content.includes("[Authorize]") || content.includes("[NavRoute(") || content.includes("[AllowAnonymous]");
|
|
10019
|
+
if (!hasClassLevelAuth) {
|
|
10020
|
+
const controllerMatch = content.match(/class\s+(\w+Controller)/);
|
|
10021
|
+
const controllerName = controllerMatch ? controllerMatch[1] : "Unknown";
|
|
10022
|
+
const lineNumber = controllerMatch ? getLineNumber(content, controllerMatch.index) : 1;
|
|
10023
|
+
result.findings.push({
|
|
10024
|
+
severity: "blocking",
|
|
10025
|
+
category: "authorization",
|
|
10026
|
+
message: `Controller ${controllerName} missing authorization attribute`,
|
|
10027
|
+
file: path20.relative(structure.root, file),
|
|
10028
|
+
line: lineNumber,
|
|
10029
|
+
suggestion: 'Add [NavRoute("context.application.module")] or [Authorize] attribute to the controller class',
|
|
10030
|
+
cweId: "CWE-862"
|
|
10031
|
+
});
|
|
10032
|
+
}
|
|
10033
|
+
}
|
|
10034
|
+
}
|
|
10035
|
+
}
|
|
10036
|
+
async function checkDangerousFunctions(structure, result) {
|
|
10037
|
+
const csharpPatterns = [
|
|
10038
|
+
{ pattern: /Process\.Start\s*\([^)]*(?:user|input|param|request)/gi, name: "Process.Start with user input" },
|
|
10039
|
+
{ pattern: /Assembly\.Load(?:From|File)?\s*\(/g, name: "Dynamic assembly loading" },
|
|
10040
|
+
{ pattern: /Type\.GetType\s*\([^)]*(?:user|input|param|request)/gi, name: "Type.GetType with user input" },
|
|
10041
|
+
{ pattern: /Activator\.CreateInstance\s*\([^)]*(?:user|input|param|request)/gi, name: "Activator.CreateInstance with user input" }
|
|
10042
|
+
];
|
|
10043
|
+
const typescriptPatterns = [
|
|
10044
|
+
{ pattern: /\beval\s*\(/g, name: "eval() usage" },
|
|
10045
|
+
{ pattern: /new\s+Function\s*\(/g, name: "new Function() usage" },
|
|
10046
|
+
{ pattern: /child_process\.exec\s*\(/g, name: "child_process.exec usage" },
|
|
10047
|
+
{ pattern: /\.innerHTML\s*=/g, name: "innerHTML assignment" },
|
|
10048
|
+
{ pattern: /dangerouslySetInnerHTML/g, name: "dangerouslySetInnerHTML usage" }
|
|
10049
|
+
];
|
|
10050
|
+
const csFiles = await getFilesToScan(structure, ["**/*.cs"]);
|
|
10051
|
+
for (const file of csFiles) {
|
|
10052
|
+
if (isExcludedFile(file)) continue;
|
|
10053
|
+
const content = await readText(file);
|
|
10054
|
+
await checkPatterns(file, content, csharpPatterns, structure, result);
|
|
10055
|
+
}
|
|
10056
|
+
const tsFiles = await getFilesToScan(structure, ["**/*.ts", "**/*.tsx"]);
|
|
10057
|
+
for (const file of tsFiles) {
|
|
10058
|
+
if (isExcludedFile(file)) continue;
|
|
10059
|
+
const content = await readText(file);
|
|
10060
|
+
await checkPatterns(file, content, typescriptPatterns, structure, result);
|
|
10061
|
+
}
|
|
10062
|
+
}
|
|
10063
|
+
async function checkPatterns(file, content, patterns, structure, result) {
|
|
10064
|
+
const lines = content.split("\n");
|
|
10065
|
+
for (const { pattern, name } of patterns) {
|
|
10066
|
+
pattern.lastIndex = 0;
|
|
10067
|
+
let match;
|
|
10068
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
10069
|
+
const lineNumber = getLineNumber(content, match.index);
|
|
10070
|
+
const lineContent = lines[lineNumber - 1]?.trim() || "";
|
|
10071
|
+
result.findings.push({
|
|
10072
|
+
severity: "critical",
|
|
10073
|
+
category: "dangerous-functions",
|
|
10074
|
+
message: `Dangerous function: ${name}`,
|
|
10075
|
+
file: path20.relative(structure.root, file),
|
|
10076
|
+
line: lineNumber,
|
|
10077
|
+
code: truncateCode(lineContent),
|
|
10078
|
+
suggestion: "Avoid using dangerous functions with user-controlled input. Use safe alternatives.",
|
|
10079
|
+
cweId: "CWE-78"
|
|
10080
|
+
});
|
|
10081
|
+
}
|
|
10082
|
+
}
|
|
10083
|
+
}
|
|
10084
|
+
async function checkInputValidation(structure, result) {
|
|
10085
|
+
if (!structure.application) {
|
|
10086
|
+
return;
|
|
10087
|
+
}
|
|
10088
|
+
const dtoFiles = await findFiles("**/*Dto.cs", { cwd: structure.application });
|
|
10089
|
+
const dtos = [];
|
|
10090
|
+
for (const file of dtoFiles) {
|
|
10091
|
+
const content = await readText(file);
|
|
10092
|
+
const dtoMatch = content.match(/class\s+(\w+Dto)/);
|
|
10093
|
+
if (dtoMatch) {
|
|
10094
|
+
dtos.push(dtoMatch[1]);
|
|
10095
|
+
}
|
|
10096
|
+
}
|
|
10097
|
+
const validatorFiles = await findFiles("**/*Validator.cs", { cwd: structure.application });
|
|
10098
|
+
const validators = [];
|
|
10099
|
+
for (const file of validatorFiles) {
|
|
10100
|
+
const content = await readText(file);
|
|
10101
|
+
const validatorMatch = content.match(/class\s+(\w+Validator)/);
|
|
10102
|
+
if (validatorMatch) {
|
|
10103
|
+
validators.push(validatorMatch[1]);
|
|
10104
|
+
}
|
|
10105
|
+
}
|
|
10106
|
+
for (const dto of dtos) {
|
|
10107
|
+
const expectedValidator = dto.replace(/Dto$/, "DtoValidator");
|
|
10108
|
+
const hasValidator = validators.some((v) => v === expectedValidator || v === `${dto}Validator`);
|
|
10109
|
+
if (!hasValidator) {
|
|
10110
|
+
result.findings.push({
|
|
10111
|
+
severity: "warning",
|
|
10112
|
+
category: "input-validation",
|
|
10113
|
+
message: `DTO ${dto} has no FluentValidation validator`,
|
|
10114
|
+
file: `Application/DTOs/${dto}.cs`,
|
|
10115
|
+
suggestion: `Create ${expectedValidator} class with FluentValidation rules`,
|
|
10116
|
+
cweId: "CWE-20"
|
|
10117
|
+
});
|
|
10118
|
+
}
|
|
10119
|
+
}
|
|
10120
|
+
}
|
|
10121
|
+
async function getFilesToScan(structure, patterns) {
|
|
10122
|
+
const allFiles = [];
|
|
10123
|
+
for (const pattern of patterns) {
|
|
10124
|
+
const files = await findFiles(pattern, { cwd: structure.root });
|
|
10125
|
+
for (const file of files) {
|
|
10126
|
+
try {
|
|
10127
|
+
validatePathSecurity(file, structure.root);
|
|
10128
|
+
allFiles.push(file);
|
|
10129
|
+
} catch {
|
|
10130
|
+
logger.warn("Skipping file outside project root", { file });
|
|
10131
|
+
}
|
|
10132
|
+
}
|
|
10133
|
+
}
|
|
10134
|
+
return [...new Set(allFiles)];
|
|
10135
|
+
}
|
|
10136
|
+
function isExcludedFile(filePath) {
|
|
10137
|
+
const exclusions = [
|
|
10138
|
+
/[/\\]bin[/\\]/,
|
|
10139
|
+
/[/\\]obj[/\\]/,
|
|
10140
|
+
/[/\\]node_modules[/\\]/,
|
|
10141
|
+
/\.example\./,
|
|
10142
|
+
/\.template\./,
|
|
10143
|
+
/\.test\./,
|
|
10144
|
+
/\.spec\./,
|
|
10145
|
+
/Tests[/\\]/,
|
|
10146
|
+
/appsettings\.Development\.json$/,
|
|
10147
|
+
/appsettings\.Template\.json$/
|
|
10148
|
+
];
|
|
10149
|
+
return exclusions.some((pattern) => pattern.test(filePath));
|
|
10150
|
+
}
|
|
10151
|
+
function isPlaceholderValue(line) {
|
|
10152
|
+
const placeholderPatterns = [
|
|
10153
|
+
/\${[A-Z_][A-Z0-9_]*}/,
|
|
10154
|
+
// ${VAR_NAME} - env var syntax
|
|
10155
|
+
/%[A-Z_][A-Z0-9_]*%/,
|
|
10156
|
+
// %VAR_NAME% - Windows env var
|
|
10157
|
+
/\{\{[A-Za-z_][A-Za-z0-9_]*\}\}/,
|
|
10158
|
+
// {{varName}} - template syntax
|
|
10159
|
+
/["']your[_-]?(?:password|secret|key|token)["']/i,
|
|
10160
|
+
// "your_password" placeholders
|
|
10161
|
+
/["']changeme["']/i,
|
|
10162
|
+
/["']<[A-Z_]+>["']/,
|
|
10163
|
+
// "<REPLACE_ME>" syntax
|
|
10164
|
+
/["']xxx+["']/i,
|
|
10165
|
+
// "xxx" or "XXXX" placeholders
|
|
10166
|
+
/=\s*["']["']/
|
|
10167
|
+
// Empty string assignments
|
|
10168
|
+
];
|
|
10169
|
+
return placeholderPatterns.some((pattern) => pattern.test(line));
|
|
10170
|
+
}
|
|
10171
|
+
function getLineNumber(content, index) {
|
|
10172
|
+
const normalizedContent = content.substring(0, index).replace(/\r\n/g, "\n");
|
|
10173
|
+
return normalizedContent.split("\n").length;
|
|
10174
|
+
}
|
|
10175
|
+
function truncateCode(code, maxLength = 80) {
|
|
10176
|
+
return code.length > maxLength ? code.substring(0, maxLength) + "..." : code;
|
|
10177
|
+
}
|
|
10178
|
+
function formatSecurityReport(result) {
|
|
10179
|
+
const lines = [];
|
|
10180
|
+
lines.push("# Security Validation Report");
|
|
10181
|
+
lines.push("");
|
|
10182
|
+
lines.push("## Summary");
|
|
10183
|
+
lines.push(`- **Status**: ${result.valid ? "\u2705 PASSED" : "\u274C FAILED"}`);
|
|
10184
|
+
lines.push(`- **Blocking**: ${result.stats.blocking}`);
|
|
10185
|
+
lines.push(`- **Critical**: ${result.stats.critical}`);
|
|
10186
|
+
lines.push(`- **Warnings**: ${result.stats.warning}`);
|
|
10187
|
+
lines.push(`- **Files Scanned**: ${result.stats.filesScanned}`);
|
|
10188
|
+
lines.push("");
|
|
10189
|
+
if (result.findings.length === 0) {
|
|
10190
|
+
lines.push("No security issues found.");
|
|
10191
|
+
return lines.join("\n");
|
|
10192
|
+
}
|
|
10193
|
+
const blocking = result.findings.filter((f) => f.severity === "blocking");
|
|
10194
|
+
const critical = result.findings.filter((f) => f.severity === "critical");
|
|
10195
|
+
const warnings = result.findings.filter((f) => f.severity === "warning");
|
|
10196
|
+
if (blocking.length > 0) {
|
|
10197
|
+
lines.push("## Blocking Issues");
|
|
10198
|
+
lines.push("");
|
|
10199
|
+
for (const finding of blocking) {
|
|
10200
|
+
formatFinding(finding, lines);
|
|
10201
|
+
}
|
|
10202
|
+
}
|
|
10203
|
+
if (critical.length > 0) {
|
|
10204
|
+
lines.push("## Critical Issues");
|
|
10205
|
+
lines.push("");
|
|
10206
|
+
for (const finding of critical) {
|
|
10207
|
+
formatFinding(finding, lines);
|
|
10208
|
+
}
|
|
10209
|
+
}
|
|
10210
|
+
if (warnings.length > 0) {
|
|
10211
|
+
lines.push("## Warnings");
|
|
10212
|
+
lines.push("");
|
|
10213
|
+
for (const finding of warnings) {
|
|
10214
|
+
formatFinding(finding, lines);
|
|
10215
|
+
}
|
|
10216
|
+
}
|
|
10217
|
+
return lines.join("\n");
|
|
10218
|
+
}
|
|
10219
|
+
function formatFinding(finding, lines) {
|
|
10220
|
+
lines.push(`### ${finding.category}: ${finding.message}`);
|
|
10221
|
+
if (finding.file) {
|
|
10222
|
+
lines.push(`- **File**: \`${finding.file}${finding.line ? `:${finding.line}` : ""}\``);
|
|
10223
|
+
}
|
|
10224
|
+
if (finding.code) {
|
|
10225
|
+
lines.push(`- **Code**: \`${finding.code}\``);
|
|
10226
|
+
}
|
|
10227
|
+
if (finding.cweId) {
|
|
10228
|
+
lines.push(`- **CWE**: ${finding.cweId}`);
|
|
10229
|
+
}
|
|
10230
|
+
lines.push(`- **Fix**: ${finding.suggestion}`);
|
|
10231
|
+
lines.push("");
|
|
10232
|
+
}
|
|
10233
|
+
|
|
10234
|
+
// src/tools/analyze-code-quality.ts
|
|
10235
|
+
import path21 from "path";
|
|
10236
|
+
var analyzeCodeQualityTool = {
|
|
10237
|
+
name: "analyze_code_quality",
|
|
10238
|
+
description: "Analyze code quality metrics for SmartStack projects: cognitive complexity, cyclomatic complexity, function size, nesting depth, and maintainability indicators.",
|
|
10239
|
+
inputSchema: {
|
|
10240
|
+
type: "object",
|
|
10241
|
+
properties: {
|
|
10242
|
+
path: {
|
|
10243
|
+
type: "string",
|
|
10244
|
+
description: "Project path to analyze (default: SmartStack.app path from config)"
|
|
10245
|
+
},
|
|
10246
|
+
metrics: {
|
|
10247
|
+
type: "array",
|
|
10248
|
+
items: {
|
|
10249
|
+
type: "string",
|
|
10250
|
+
enum: [
|
|
10251
|
+
"cognitive-complexity",
|
|
10252
|
+
"cyclomatic-complexity",
|
|
10253
|
+
"function-size",
|
|
10254
|
+
"nesting-depth",
|
|
10255
|
+
"parameter-count",
|
|
10256
|
+
"code-duplication",
|
|
10257
|
+
"file-size",
|
|
10258
|
+
"all"
|
|
10259
|
+
]
|
|
10260
|
+
},
|
|
10261
|
+
description: "Metrics to analyze",
|
|
10262
|
+
default: ["all"]
|
|
10263
|
+
},
|
|
10264
|
+
threshold: {
|
|
10265
|
+
type: "string",
|
|
10266
|
+
enum: ["strict", "normal", "lenient"],
|
|
10267
|
+
description: "Threshold level for violations",
|
|
10268
|
+
default: "normal"
|
|
10269
|
+
},
|
|
10270
|
+
scope: {
|
|
10271
|
+
type: "string",
|
|
10272
|
+
enum: ["changed", "all"],
|
|
10273
|
+
description: "Analyze only changed files or all",
|
|
10274
|
+
default: "all"
|
|
10275
|
+
}
|
|
10276
|
+
}
|
|
10277
|
+
}
|
|
10278
|
+
};
|
|
10279
|
+
var THRESHOLDS = {
|
|
10280
|
+
strict: {
|
|
10281
|
+
cognitiveComplexity: 10,
|
|
10282
|
+
cyclomaticComplexity: 8,
|
|
10283
|
+
functionSize: 30,
|
|
10284
|
+
nestingDepth: 2,
|
|
10285
|
+
parameterCount: 3,
|
|
10286
|
+
fileSize: 300
|
|
10287
|
+
},
|
|
10288
|
+
normal: {
|
|
10289
|
+
cognitiveComplexity: 15,
|
|
10290
|
+
cyclomaticComplexity: 10,
|
|
10291
|
+
functionSize: 50,
|
|
10292
|
+
nestingDepth: 3,
|
|
10293
|
+
parameterCount: 4,
|
|
10294
|
+
fileSize: 500
|
|
10295
|
+
},
|
|
10296
|
+
lenient: {
|
|
10297
|
+
cognitiveComplexity: 25,
|
|
10298
|
+
cyclomaticComplexity: 15,
|
|
10299
|
+
functionSize: 80,
|
|
10300
|
+
nestingDepth: 4,
|
|
10301
|
+
parameterCount: 5,
|
|
10302
|
+
fileSize: 800
|
|
10303
|
+
}
|
|
10304
|
+
};
|
|
10305
|
+
async function handleAnalyzeCodeQuality(args, config) {
|
|
10306
|
+
const input = AnalyzeCodeQualityInputSchema.parse(args);
|
|
10307
|
+
const projectPath = input.path || config.smartstack.projectPath;
|
|
10308
|
+
const thresholdLevel = input.threshold;
|
|
10309
|
+
const thresholds = THRESHOLDS[thresholdLevel];
|
|
10310
|
+
logger.info("Analyzing code quality", { projectPath, threshold: thresholdLevel });
|
|
10311
|
+
const structure = await findSmartStackStructure(projectPath);
|
|
10312
|
+
const allFunctionMetrics = [];
|
|
10313
|
+
const fileMetrics = /* @__PURE__ */ new Map();
|
|
10314
|
+
const csFiles = await findFiles("**/*.cs", { cwd: structure.root });
|
|
10315
|
+
const filteredCsFiles = csFiles.filter((f) => !isExcludedPath(f));
|
|
10316
|
+
for (const file of filteredCsFiles) {
|
|
10317
|
+
const content = await readText(file);
|
|
10318
|
+
const relPath = path21.relative(structure.root, file);
|
|
10319
|
+
const lineCount = content.split("\n").length;
|
|
10320
|
+
const functions = extractCSharpFunctions(content, relPath);
|
|
10321
|
+
allFunctionMetrics.push(...functions);
|
|
10322
|
+
fileMetrics.set(relPath, { lineCount, functions: functions.length });
|
|
10323
|
+
}
|
|
10324
|
+
const tsFiles = await findFiles("**/*.{ts,tsx}", { cwd: structure.root });
|
|
10325
|
+
const filteredTsFiles = tsFiles.filter((f) => !isExcludedPath(f));
|
|
10326
|
+
for (const file of filteredTsFiles) {
|
|
10327
|
+
const content = await readText(file);
|
|
10328
|
+
const relPath = path21.relative(structure.root, file);
|
|
10329
|
+
const lineCount = content.split("\n").length;
|
|
10330
|
+
const functions = extractTypeScriptFunctions(content, relPath);
|
|
10331
|
+
allFunctionMetrics.push(...functions);
|
|
10332
|
+
fileMetrics.set(relPath, { lineCount, functions: functions.length });
|
|
10333
|
+
}
|
|
10334
|
+
const metrics = calculateMetrics(allFunctionMetrics, fileMetrics, thresholds);
|
|
10335
|
+
const hotspots = identifyHotspots(allFunctionMetrics, fileMetrics, thresholds);
|
|
10336
|
+
const summary = calculateSummary(allFunctionMetrics, fileMetrics, metrics, hotspots);
|
|
10337
|
+
const result = {
|
|
10338
|
+
summary,
|
|
10339
|
+
metrics,
|
|
10340
|
+
hotspots
|
|
10341
|
+
};
|
|
10342
|
+
return formatQualityReport(result, thresholds);
|
|
10343
|
+
}
|
|
10344
|
+
function extractCSharpFunctions(content, file) {
|
|
10345
|
+
const functions = [];
|
|
10346
|
+
const lines = content.split("\n");
|
|
10347
|
+
const methodPattern = /\b(public|private|protected|internal)\s+(?:async\s+)?(?:static\s+)?(?:virtual\s+)?(?:override\s+)?(?:[\w<>,\s\[\]]+)\s+(\w+)\s*\(([^)]*)\)/gm;
|
|
10348
|
+
let match;
|
|
10349
|
+
while ((match = methodPattern.exec(content)) !== null) {
|
|
10350
|
+
const methodName = match[2];
|
|
10351
|
+
const params = match[3];
|
|
10352
|
+
const startLine = getLineNumber2(content, match.index);
|
|
10353
|
+
const endLine = findMethodEnd(lines, startLine - 1);
|
|
10354
|
+
const lineCount = endLine - startLine + 1;
|
|
10355
|
+
const parameterCount = params.trim() ? params.split(",").length : 0;
|
|
10356
|
+
const methodBody = lines.slice(startLine - 1, endLine).join("\n");
|
|
10357
|
+
const cognitiveComplexity = calculateCognitiveComplexity(methodBody);
|
|
10358
|
+
const cyclomaticComplexity = calculateCyclomaticComplexity(methodBody);
|
|
10359
|
+
const maxNestingDepth = calculateNestingDepth(methodBody);
|
|
10360
|
+
functions.push({
|
|
10361
|
+
name: methodName,
|
|
10362
|
+
file,
|
|
10363
|
+
startLine,
|
|
10364
|
+
endLine,
|
|
10365
|
+
lineCount,
|
|
10366
|
+
parameterCount,
|
|
10367
|
+
maxNestingDepth,
|
|
10368
|
+
cognitiveComplexity,
|
|
10369
|
+
cyclomaticComplexity
|
|
10370
|
+
});
|
|
10371
|
+
}
|
|
10372
|
+
return functions;
|
|
10373
|
+
}
|
|
10374
|
+
function extractTypeScriptFunctions(content, file) {
|
|
10375
|
+
const functions = [];
|
|
10376
|
+
const lines = content.split("\n");
|
|
10377
|
+
const processedFunctions = /* @__PURE__ */ new Set();
|
|
10378
|
+
const patterns = [
|
|
10379
|
+
// Arrow functions assigned to const/let
|
|
10380
|
+
/(?:const|let)\s+(\w+)\s*=\s*(?:async\s+)?\([^)]*\)\s*(?::\s*\w+)?\s*=>/g,
|
|
10381
|
+
// Function declarations
|
|
10382
|
+
/(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/g,
|
|
10383
|
+
// Class methods (removed ^ anchor for consistency)
|
|
10384
|
+
/\b(?:async\s+)?(\w+)\s*\(([^)]*)\)\s*(?::\s*[\w<>,\s\[\]]+)?\s*\{/gm
|
|
10385
|
+
];
|
|
10386
|
+
for (const pattern of patterns) {
|
|
10387
|
+
pattern.lastIndex = 0;
|
|
10388
|
+
let match;
|
|
10389
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
10390
|
+
const funcName = match[1];
|
|
10391
|
+
const params = match[2] || "";
|
|
10392
|
+
const startLine = getLineNumber2(content, match.index);
|
|
10393
|
+
if (["if", "for", "while", "switch", "catch", "constructor"].includes(funcName)) continue;
|
|
10394
|
+
const funcKey = `${file}:${startLine}:${funcName}`;
|
|
10395
|
+
if (processedFunctions.has(funcKey)) continue;
|
|
10396
|
+
processedFunctions.add(funcKey);
|
|
10397
|
+
const endLine = findMethodEnd(lines, startLine - 1);
|
|
10398
|
+
const lineCount = endLine - startLine + 1;
|
|
10399
|
+
const parameterCount = params.trim() ? params.split(",").length : 0;
|
|
10400
|
+
const methodBody = lines.slice(startLine - 1, endLine).join("\n");
|
|
10401
|
+
const cognitiveComplexity = calculateCognitiveComplexity(methodBody);
|
|
10402
|
+
const cyclomaticComplexity = calculateCyclomaticComplexity(methodBody);
|
|
10403
|
+
const maxNestingDepth = calculateNestingDepth(methodBody);
|
|
10404
|
+
functions.push({
|
|
10405
|
+
name: funcName,
|
|
10406
|
+
file,
|
|
10407
|
+
startLine,
|
|
10408
|
+
endLine,
|
|
10409
|
+
lineCount,
|
|
10410
|
+
parameterCount,
|
|
10411
|
+
maxNestingDepth,
|
|
10412
|
+
cognitiveComplexity,
|
|
10413
|
+
cyclomaticComplexity
|
|
10414
|
+
});
|
|
10415
|
+
}
|
|
10416
|
+
}
|
|
10417
|
+
return functions;
|
|
10418
|
+
}
|
|
10419
|
+
function findMethodEnd(lines, startIndex) {
|
|
10420
|
+
let braceCount = 0;
|
|
10421
|
+
let foundFirstBrace = false;
|
|
10422
|
+
for (let i = startIndex; i < lines.length; i++) {
|
|
10423
|
+
const line = stripStringsAndComments(lines[i]);
|
|
10424
|
+
for (const char of line) {
|
|
10425
|
+
if (char === "{") {
|
|
10426
|
+
braceCount++;
|
|
10427
|
+
foundFirstBrace = true;
|
|
10428
|
+
} else if (char === "}") {
|
|
10429
|
+
braceCount--;
|
|
10430
|
+
if (foundFirstBrace && braceCount === 0) {
|
|
10431
|
+
return i + 1;
|
|
10432
|
+
}
|
|
10433
|
+
}
|
|
10434
|
+
}
|
|
10435
|
+
}
|
|
10436
|
+
return startIndex + 1;
|
|
10437
|
+
}
|
|
10438
|
+
function stripStringsAndComments(line) {
|
|
10439
|
+
let result = line.replace(/\/\/.*$/, "");
|
|
10440
|
+
result = result.replace(/"(?:[^"\\]|\\.)*"/g, '""');
|
|
10441
|
+
result = result.replace(/'(?:[^'\\]|\\.)*'/g, "''");
|
|
10442
|
+
result = result.replace(/`(?:[^`\\]|\\.)*`/g, "``");
|
|
10443
|
+
return result;
|
|
10444
|
+
}
|
|
10445
|
+
function calculateCognitiveComplexity(code) {
|
|
10446
|
+
let complexity = 0;
|
|
10447
|
+
let nestingLevel = 0;
|
|
10448
|
+
const lines = code.split("\n");
|
|
10449
|
+
for (const line of lines) {
|
|
10450
|
+
const trimmed = line.trim();
|
|
10451
|
+
const controlFlowPatterns = [
|
|
10452
|
+
/\bif\s*\(/g,
|
|
10453
|
+
/\belse\s+if\s*\(/g,
|
|
10454
|
+
/\bfor\s*\(/g,
|
|
10455
|
+
/\bforeach\s*\(/g,
|
|
10456
|
+
/\bwhile\s*\(/g,
|
|
10457
|
+
/\bdo\s*\{/g,
|
|
10458
|
+
/\bswitch\s*\(/g,
|
|
10459
|
+
/\bcatch\s*\(/g
|
|
10460
|
+
];
|
|
10461
|
+
for (const pattern of controlFlowPatterns) {
|
|
10462
|
+
const matches = trimmed.match(pattern) || [];
|
|
10463
|
+
complexity += matches.length * (1 + nestingLevel);
|
|
10464
|
+
}
|
|
10465
|
+
const logicalPatterns = [/&&/g, /\|\|/g, /\?\?/g, /\?\./g];
|
|
10466
|
+
for (const pattern of logicalPatterns) {
|
|
10467
|
+
const matches = trimmed.match(pattern) || [];
|
|
10468
|
+
complexity += matches.length;
|
|
10469
|
+
}
|
|
10470
|
+
const ternaryMatches = trimmed.match(/\?[^?:]+:/g) || [];
|
|
10471
|
+
complexity += ternaryMatches.length;
|
|
10472
|
+
const openBraces = (trimmed.match(/\{/g) || []).length;
|
|
10473
|
+
const closeBraces = (trimmed.match(/\}/g) || []).length;
|
|
10474
|
+
nestingLevel += openBraces - closeBraces;
|
|
10475
|
+
nestingLevel = Math.max(0, nestingLevel);
|
|
10476
|
+
}
|
|
10477
|
+
return complexity;
|
|
10478
|
+
}
|
|
10479
|
+
function calculateCyclomaticComplexity(code) {
|
|
10480
|
+
let complexity = 1;
|
|
10481
|
+
const patterns = [
|
|
10482
|
+
/\bif\b/g,
|
|
10483
|
+
/\belse\s+if\b/g,
|
|
10484
|
+
/\bwhile\b/g,
|
|
10485
|
+
/\bfor\b/g,
|
|
10486
|
+
/\bforeach\b/g,
|
|
10487
|
+
/\bcase\b/g,
|
|
10488
|
+
/\bcatch\b/g,
|
|
10489
|
+
/\?\?/g,
|
|
10490
|
+
/\?\./g,
|
|
10491
|
+
/&&/g,
|
|
10492
|
+
/\|\|/g
|
|
10493
|
+
];
|
|
10494
|
+
for (const pattern of patterns) {
|
|
10495
|
+
const matches = code.match(pattern) || [];
|
|
10496
|
+
complexity += matches.length;
|
|
10497
|
+
}
|
|
10498
|
+
return complexity;
|
|
10499
|
+
}
|
|
10500
|
+
function calculateNestingDepth(code) {
|
|
10501
|
+
let maxDepth = 0;
|
|
10502
|
+
let currentDepth = 0;
|
|
10503
|
+
for (const char of code) {
|
|
10504
|
+
if (char === "{") {
|
|
10505
|
+
currentDepth++;
|
|
10506
|
+
maxDepth = Math.max(maxDepth, currentDepth);
|
|
10507
|
+
} else if (char === "}") {
|
|
10508
|
+
currentDepth = Math.max(0, currentDepth - 1);
|
|
10509
|
+
}
|
|
10510
|
+
}
|
|
10511
|
+
return maxDepth;
|
|
10512
|
+
}
|
|
10513
|
+
function calculateMetrics(functions, fileMetrics, thresholds) {
|
|
10514
|
+
const createStat = (values, threshold) => {
|
|
10515
|
+
const filtered = values.filter((v) => v > 0);
|
|
10516
|
+
const avg = filtered.length > 0 ? filtered.reduce((a, b) => a + b, 0) / filtered.length : 0;
|
|
10517
|
+
const max = filtered.length > 0 ? Math.max(...filtered) : 0;
|
|
10518
|
+
const violations = filtered.filter((v) => v > threshold).length;
|
|
10519
|
+
return {
|
|
10520
|
+
average: Math.round(avg * 10) / 10,
|
|
10521
|
+
max,
|
|
10522
|
+
threshold,
|
|
10523
|
+
violations
|
|
10524
|
+
};
|
|
10525
|
+
};
|
|
10526
|
+
const fileSizes = Array.from(fileMetrics.values()).map((f) => f.lineCount);
|
|
10527
|
+
return {
|
|
10528
|
+
cognitiveComplexity: createStat(
|
|
10529
|
+
functions.map((f) => f.cognitiveComplexity),
|
|
10530
|
+
thresholds.cognitiveComplexity
|
|
10531
|
+
),
|
|
10532
|
+
cyclomaticComplexity: createStat(
|
|
10533
|
+
functions.map((f) => f.cyclomaticComplexity),
|
|
10534
|
+
thresholds.cyclomaticComplexity
|
|
10535
|
+
),
|
|
10536
|
+
functionSize: createStat(
|
|
10537
|
+
functions.map((f) => f.lineCount),
|
|
10538
|
+
thresholds.functionSize
|
|
10539
|
+
),
|
|
10540
|
+
nestingDepth: createStat(
|
|
10541
|
+
functions.map((f) => f.maxNestingDepth),
|
|
10542
|
+
thresholds.nestingDepth
|
|
10543
|
+
),
|
|
10544
|
+
fileSize: createStat(fileSizes, thresholds.fileSize)
|
|
10545
|
+
};
|
|
10546
|
+
}
|
|
10547
|
+
function identifyHotspots(functions, fileMetrics, thresholds) {
|
|
10548
|
+
const hotspots = [];
|
|
10549
|
+
for (const func of functions) {
|
|
10550
|
+
const issues = [];
|
|
10551
|
+
if (func.cognitiveComplexity > thresholds.cognitiveComplexity) {
|
|
10552
|
+
issues.push(`Cognitive complexity: ${func.cognitiveComplexity} (threshold: ${thresholds.cognitiveComplexity})`);
|
|
10553
|
+
}
|
|
10554
|
+
if (func.cyclomaticComplexity > thresholds.cyclomaticComplexity) {
|
|
10555
|
+
issues.push(`Cyclomatic complexity: ${func.cyclomaticComplexity} (threshold: ${thresholds.cyclomaticComplexity})`);
|
|
10556
|
+
}
|
|
10557
|
+
if (func.lineCount > thresholds.functionSize) {
|
|
10558
|
+
issues.push(`Lines: ${func.lineCount} (threshold: ${thresholds.functionSize})`);
|
|
10559
|
+
}
|
|
10560
|
+
if (func.maxNestingDepth > thresholds.nestingDepth) {
|
|
10561
|
+
issues.push(`Nesting depth: ${func.maxNestingDepth} (threshold: ${thresholds.nestingDepth})`);
|
|
10562
|
+
}
|
|
10563
|
+
if (issues.length > 0) {
|
|
10564
|
+
const severity = issues.length >= 3 ? "high" : issues.length === 2 ? "medium" : "low";
|
|
10565
|
+
hotspots.push({
|
|
10566
|
+
file: func.file,
|
|
10567
|
+
function: func.name,
|
|
10568
|
+
issues,
|
|
10569
|
+
severity,
|
|
10570
|
+
metrics: {
|
|
10571
|
+
cognitiveComplexity: func.cognitiveComplexity,
|
|
10572
|
+
cyclomaticComplexity: func.cyclomaticComplexity,
|
|
10573
|
+
lineCount: func.lineCount,
|
|
10574
|
+
nestingDepth: func.maxNestingDepth
|
|
10575
|
+
}
|
|
10576
|
+
});
|
|
10577
|
+
}
|
|
10578
|
+
}
|
|
10579
|
+
for (const [file, metrics] of fileMetrics) {
|
|
10580
|
+
if (metrics.lineCount > thresholds.fileSize) {
|
|
10581
|
+
hotspots.push({
|
|
10582
|
+
file,
|
|
10583
|
+
issues: [`File size: ${metrics.lineCount} lines (threshold: ${thresholds.fileSize})`],
|
|
10584
|
+
severity: "medium",
|
|
10585
|
+
metrics: {
|
|
10586
|
+
lineCount: metrics.lineCount
|
|
10587
|
+
}
|
|
10588
|
+
});
|
|
10589
|
+
}
|
|
10590
|
+
}
|
|
10591
|
+
const severityOrder = { high: 0, medium: 1, low: 2 };
|
|
10592
|
+
hotspots.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
|
|
10593
|
+
return hotspots.slice(0, 20);
|
|
10594
|
+
}
|
|
10595
|
+
function calculateSummary(functions, fileMetrics, metrics, hotspots) {
|
|
10596
|
+
const totalViolations = metrics.cognitiveComplexity.violations + metrics.cyclomaticComplexity.violations + metrics.functionSize.violations + metrics.nestingDepth.violations + metrics.fileSize.violations;
|
|
10597
|
+
const totalFunctions = functions.length;
|
|
10598
|
+
const totalFiles = fileMetrics.size;
|
|
10599
|
+
const violationRate = totalFunctions > 0 ? totalViolations / totalFunctions : 0;
|
|
10600
|
+
const score = Math.max(0, Math.round(100 - violationRate * 100));
|
|
10601
|
+
let grade;
|
|
10602
|
+
if (score >= 90) grade = "A";
|
|
10603
|
+
else if (score >= 80) grade = "B";
|
|
10604
|
+
else if (score >= 70) grade = "C";
|
|
10605
|
+
else if (score >= 60) grade = "D";
|
|
10606
|
+
else grade = "F";
|
|
10607
|
+
return {
|
|
10608
|
+
score,
|
|
10609
|
+
grade,
|
|
10610
|
+
filesAnalyzed: totalFiles,
|
|
10611
|
+
functionsAnalyzed: totalFunctions,
|
|
10612
|
+
issuesFound: hotspots.length
|
|
10613
|
+
};
|
|
10614
|
+
}
|
|
10615
|
+
function isExcludedPath(filePath) {
|
|
10616
|
+
const exclusions = [
|
|
10617
|
+
/[/\\]bin[/\\]/,
|
|
10618
|
+
/[/\\]obj[/\\]/,
|
|
10619
|
+
/[/\\]node_modules[/\\]/,
|
|
10620
|
+
/[/\\]Migrations[/\\]/,
|
|
10621
|
+
/\.test\./,
|
|
10622
|
+
/\.spec\./,
|
|
10623
|
+
/Tests[/\\]/,
|
|
10624
|
+
/\.d\.ts$/,
|
|
10625
|
+
/\.min\./
|
|
10626
|
+
];
|
|
10627
|
+
return exclusions.some((pattern) => pattern.test(filePath));
|
|
10628
|
+
}
|
|
10629
|
+
function getLineNumber2(content, index) {
|
|
10630
|
+
const normalizedContent = content.substring(0, index).replace(/\r\n/g, "\n");
|
|
10631
|
+
return normalizedContent.split("\n").length;
|
|
10632
|
+
}
|
|
10633
|
+
function formatQualityReport(result, thresholds) {
|
|
10634
|
+
const lines = [];
|
|
10635
|
+
lines.push("# Code Quality Report");
|
|
10636
|
+
lines.push("");
|
|
10637
|
+
lines.push("## Summary");
|
|
10638
|
+
lines.push(`- **Score**: ${result.summary.score}/100 (Grade: ${result.summary.grade})`);
|
|
10639
|
+
lines.push(`- **Files analyzed**: ${result.summary.filesAnalyzed}`);
|
|
10640
|
+
lines.push(`- **Functions analyzed**: ${result.summary.functionsAnalyzed}`);
|
|
10641
|
+
lines.push(`- **Issues found**: ${result.summary.issuesFound}`);
|
|
10642
|
+
lines.push("");
|
|
10643
|
+
lines.push("## Metrics Overview");
|
|
10644
|
+
lines.push("");
|
|
10645
|
+
lines.push("| Metric | Average | Max | Threshold | Status |");
|
|
10646
|
+
lines.push("|--------|---------|-----|-----------|--------|");
|
|
10647
|
+
const formatMetricRow = (name, stat2) => {
|
|
10648
|
+
const status = stat2.violations > 0 ? `${stat2.violations} violations` : "\u2705 OK";
|
|
10649
|
+
return `| ${name} | ${stat2.average} | ${stat2.max} | ${stat2.threshold} | ${status} |`;
|
|
10650
|
+
};
|
|
10651
|
+
lines.push(formatMetricRow("Cognitive Complexity", result.metrics.cognitiveComplexity));
|
|
10652
|
+
lines.push(formatMetricRow("Cyclomatic Complexity", result.metrics.cyclomaticComplexity));
|
|
10653
|
+
lines.push(formatMetricRow("Function Size", result.metrics.functionSize));
|
|
10654
|
+
lines.push(formatMetricRow("Nesting Depth", result.metrics.nestingDepth));
|
|
10655
|
+
lines.push(formatMetricRow("File Size", result.metrics.fileSize));
|
|
10656
|
+
lines.push("");
|
|
10657
|
+
if (result.hotspots.length > 0) {
|
|
10658
|
+
lines.push("## Hotspots (Needs Attention)");
|
|
10659
|
+
lines.push("");
|
|
10660
|
+
for (const hotspot of result.hotspots) {
|
|
10661
|
+
const severityEmoji = hotspot.severity === "high" ? "\u{1F534}" : hotspot.severity === "medium" ? "\u{1F7E1}" : "\u{1F7E2}";
|
|
10662
|
+
const location = hotspot.function ? `\`${hotspot.function}\` (${hotspot.file})` : `\`${hotspot.file}\``;
|
|
10663
|
+
lines.push(`### ${severityEmoji} ${hotspot.severity.toUpperCase()}: ${location}`);
|
|
10664
|
+
for (const issue of hotspot.issues) {
|
|
10665
|
+
lines.push(`- ${issue}`);
|
|
10666
|
+
}
|
|
10667
|
+
if (hotspot.function) {
|
|
10668
|
+
if (hotspot.metrics.cognitiveComplexity && hotspot.metrics.cognitiveComplexity > thresholds.cognitiveComplexity) {
|
|
10669
|
+
lines.push(`- **Recommendation**: Extract logic into smaller, focused methods`);
|
|
10670
|
+
} else if (hotspot.metrics.lineCount && hotspot.metrics.lineCount > thresholds.functionSize) {
|
|
10671
|
+
lines.push(`- **Recommendation**: Split into multiple functions with single responsibilities`);
|
|
10672
|
+
}
|
|
10673
|
+
} else {
|
|
10674
|
+
lines.push(`- **Recommendation**: Consider splitting this file into smaller modules`);
|
|
10675
|
+
}
|
|
10676
|
+
lines.push("");
|
|
10677
|
+
}
|
|
10678
|
+
} else {
|
|
10679
|
+
lines.push("No hotspots found. Code quality is within acceptable thresholds.");
|
|
10680
|
+
}
|
|
10681
|
+
return lines.join("\n");
|
|
10682
|
+
}
|
|
10683
|
+
|
|
9738
10684
|
// src/resources/conventions.ts
|
|
9739
10685
|
var conventionsResourceTemplate = {
|
|
9740
10686
|
uri: "smartstack://conventions",
|
|
@@ -10870,7 +11816,7 @@ Run specific or all checks:
|
|
|
10870
11816
|
}
|
|
10871
11817
|
|
|
10872
11818
|
// src/resources/project-info.ts
|
|
10873
|
-
import
|
|
11819
|
+
import path22 from "path";
|
|
10874
11820
|
var projectInfoResourceTemplate = {
|
|
10875
11821
|
uri: "smartstack://project",
|
|
10876
11822
|
name: "SmartStack Project Info",
|
|
@@ -10907,16 +11853,16 @@ async function getProjectInfoResource(config) {
|
|
|
10907
11853
|
lines.push("```");
|
|
10908
11854
|
lines.push(`${projectInfo.name}/`);
|
|
10909
11855
|
if (structure.domain) {
|
|
10910
|
-
lines.push(`\u251C\u2500\u2500 ${
|
|
11856
|
+
lines.push(`\u251C\u2500\u2500 ${path22.basename(structure.domain)}/ # Domain layer (entities)`);
|
|
10911
11857
|
}
|
|
10912
11858
|
if (structure.application) {
|
|
10913
|
-
lines.push(`\u251C\u2500\u2500 ${
|
|
11859
|
+
lines.push(`\u251C\u2500\u2500 ${path22.basename(structure.application)}/ # Application layer (services)`);
|
|
10914
11860
|
}
|
|
10915
11861
|
if (structure.infrastructure) {
|
|
10916
|
-
lines.push(`\u251C\u2500\u2500 ${
|
|
11862
|
+
lines.push(`\u251C\u2500\u2500 ${path22.basename(structure.infrastructure)}/ # Infrastructure (EF Core)`);
|
|
10917
11863
|
}
|
|
10918
11864
|
if (structure.api) {
|
|
10919
|
-
lines.push(`\u251C\u2500\u2500 ${
|
|
11865
|
+
lines.push(`\u251C\u2500\u2500 ${path22.basename(structure.api)}/ # API layer (controllers)`);
|
|
10920
11866
|
}
|
|
10921
11867
|
if (structure.web) {
|
|
10922
11868
|
lines.push(`\u2514\u2500\u2500 web/smartstack-web/ # React frontend`);
|
|
@@ -10929,8 +11875,8 @@ async function getProjectInfoResource(config) {
|
|
|
10929
11875
|
lines.push("| Project | Path |");
|
|
10930
11876
|
lines.push("|---------|------|");
|
|
10931
11877
|
for (const csproj of projectInfo.csprojFiles) {
|
|
10932
|
-
const name =
|
|
10933
|
-
const relativePath =
|
|
11878
|
+
const name = path22.basename(csproj, ".csproj");
|
|
11879
|
+
const relativePath = path22.relative(projectPath, csproj);
|
|
10934
11880
|
lines.push(`| ${name} | \`${relativePath}\` |`);
|
|
10935
11881
|
}
|
|
10936
11882
|
lines.push("");
|
|
@@ -10940,10 +11886,10 @@ async function getProjectInfoResource(config) {
|
|
|
10940
11886
|
cwd: structure.migrations,
|
|
10941
11887
|
ignore: ["*.Designer.cs"]
|
|
10942
11888
|
});
|
|
10943
|
-
const migrations = migrationFiles.map((f) =>
|
|
11889
|
+
const migrations = migrationFiles.map((f) => path22.basename(f)).filter((f) => !f.includes("ModelSnapshot") && !f.includes(".Designer.")).sort();
|
|
10944
11890
|
lines.push("## EF Core Migrations");
|
|
10945
11891
|
lines.push("");
|
|
10946
|
-
lines.push(`**Location**: \`${
|
|
11892
|
+
lines.push(`**Location**: \`${path22.relative(projectPath, structure.migrations)}\``);
|
|
10947
11893
|
lines.push(`**Total Migrations**: ${migrations.length}`);
|
|
10948
11894
|
lines.push("");
|
|
10949
11895
|
if (migrations.length > 0) {
|
|
@@ -10978,11 +11924,11 @@ async function getProjectInfoResource(config) {
|
|
|
10978
11924
|
lines.push("dotnet build");
|
|
10979
11925
|
lines.push("");
|
|
10980
11926
|
lines.push("# Run API");
|
|
10981
|
-
lines.push(`cd ${structure.api ?
|
|
11927
|
+
lines.push(`cd ${structure.api ? path22.relative(projectPath, structure.api) : "src/Api"}`);
|
|
10982
11928
|
lines.push("dotnet run");
|
|
10983
11929
|
lines.push("");
|
|
10984
11930
|
lines.push("# Run frontend");
|
|
10985
|
-
lines.push(`cd ${structure.web ?
|
|
11931
|
+
lines.push(`cd ${structure.web ? path22.relative(projectPath, structure.web) : "web"}`);
|
|
10986
11932
|
lines.push("npm run dev");
|
|
10987
11933
|
lines.push("");
|
|
10988
11934
|
lines.push("# Create migration");
|
|
@@ -11005,7 +11951,7 @@ async function getProjectInfoResource(config) {
|
|
|
11005
11951
|
}
|
|
11006
11952
|
|
|
11007
11953
|
// src/resources/api-endpoints.ts
|
|
11008
|
-
import
|
|
11954
|
+
import path23 from "path";
|
|
11009
11955
|
var apiEndpointsResourceTemplate = {
|
|
11010
11956
|
uri: "smartstack://api/",
|
|
11011
11957
|
name: "SmartStack API Endpoints",
|
|
@@ -11030,7 +11976,7 @@ async function getApiEndpointsResource(config, endpointFilter) {
|
|
|
11030
11976
|
}
|
|
11031
11977
|
async function parseController(filePath, _rootPath) {
|
|
11032
11978
|
const content = await readText(filePath);
|
|
11033
|
-
const fileName =
|
|
11979
|
+
const fileName = path23.basename(filePath, ".cs");
|
|
11034
11980
|
const controllerName = fileName.replace("Controller", "");
|
|
11035
11981
|
const endpoints = [];
|
|
11036
11982
|
const routeMatch = content.match(/\[Route\s*\(\s*"([^"]+)"\s*\)\]/);
|
|
@@ -11177,7 +12123,7 @@ function getMethodEmoji(method) {
|
|
|
11177
12123
|
}
|
|
11178
12124
|
|
|
11179
12125
|
// src/resources/db-schema.ts
|
|
11180
|
-
import
|
|
12126
|
+
import path24 from "path";
|
|
11181
12127
|
var dbSchemaResourceTemplate = {
|
|
11182
12128
|
uri: "smartstack://schema/",
|
|
11183
12129
|
name: "SmartStack Database Schema",
|
|
@@ -11267,7 +12213,7 @@ async function parseEntity(filePath, rootPath, _config) {
|
|
|
11267
12213
|
tableName,
|
|
11268
12214
|
properties,
|
|
11269
12215
|
relationships,
|
|
11270
|
-
file:
|
|
12216
|
+
file: path24.relative(rootPath, filePath)
|
|
11271
12217
|
};
|
|
11272
12218
|
}
|
|
11273
12219
|
async function enrichFromConfigurations(entities, infrastructurePath, _config) {
|
|
@@ -11413,7 +12359,7 @@ function formatSchema(entities, filter, _config) {
|
|
|
11413
12359
|
}
|
|
11414
12360
|
|
|
11415
12361
|
// src/resources/entities.ts
|
|
11416
|
-
import
|
|
12362
|
+
import path25 from "path";
|
|
11417
12363
|
var entitiesResourceTemplate = {
|
|
11418
12364
|
uri: "smartstack://entities/",
|
|
11419
12365
|
name: "SmartStack Entities",
|
|
@@ -11473,7 +12419,7 @@ async function parseEntitySummary(filePath, rootPath, config) {
|
|
|
11473
12419
|
hasSoftDelete,
|
|
11474
12420
|
hasRowVersion,
|
|
11475
12421
|
file: filePath,
|
|
11476
|
-
relativePath:
|
|
12422
|
+
relativePath: path25.relative(rootPath, filePath)
|
|
11477
12423
|
};
|
|
11478
12424
|
}
|
|
11479
12425
|
function inferTableInfo(entityName, config) {
|
|
@@ -11673,7 +12619,10 @@ async function createServer() {
|
|
|
11673
12619
|
validateFrontendRoutesTool,
|
|
11674
12620
|
// Frontend Extension Tools
|
|
11675
12621
|
scaffoldFrontendExtensionTool,
|
|
11676
|
-
analyzeExtensionPointsTool
|
|
12622
|
+
analyzeExtensionPointsTool,
|
|
12623
|
+
// Security & Code Quality Tools
|
|
12624
|
+
validateSecurityTool,
|
|
12625
|
+
analyzeCodeQualityTool
|
|
11677
12626
|
]
|
|
11678
12627
|
};
|
|
11679
12628
|
});
|
|
@@ -11732,6 +12681,13 @@ async function createServer() {
|
|
|
11732
12681
|
case "analyze_extension_points":
|
|
11733
12682
|
result = await handleAnalyzeExtensionPoints(args ?? {}, config);
|
|
11734
12683
|
break;
|
|
12684
|
+
// Security & Code Quality Tools
|
|
12685
|
+
case "validate_security":
|
|
12686
|
+
result = await handleValidateSecurity(args ?? {}, config);
|
|
12687
|
+
break;
|
|
12688
|
+
case "analyze_code_quality":
|
|
12689
|
+
result = await handleAnalyzeCodeQuality(args ?? {}, config);
|
|
12690
|
+
break;
|
|
11735
12691
|
default:
|
|
11736
12692
|
throw new Error(`Unknown tool: ${name}`);
|
|
11737
12693
|
}
|