secufusion-mcp 1.2.7 → 2.0.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/.claude/settings.json +9 -0
- package/.claude-plugin/plugin.json +5 -6
- package/.mcp.json +19 -0
- package/{AGENTS.md → agents/AGENTS.md} +3 -3
- package/agents/claude.md +50 -0
- package/agents/coder.md +80 -0
- package/agents/planner.md +354 -0
- package/agents/reviewer.md +384 -0
- package/commands/analyze.md +15 -0
- package/commands/blast-radius.md +14 -0
- package/commands/map-architecture.md +10 -0
- package/commands/map-services.md +3 -0
- package/commands/watch-dna.md +9 -0
- package/hooks/hooks.json +17 -0
- package/hooks/knowledge-drift.sh +48 -0
- package/mcp/dist/parsers/api.js +30 -0
- package/mcp/dist/parsers/backend-node.js +26 -0
- package/mcp/dist/parsers/config.js +74 -0
- package/mcp/dist/parsers/database.js +40 -0
- package/mcp/dist/parsers/domain.js +21 -0
- package/mcp/dist/parsers/events.js +36 -0
- package/mcp/dist/parsers/frontend.js +34 -0
- package/mcp/dist/parsers/infra.js +42 -0
- package/mcp/dist/parsers/patterns.js +49 -0
- package/mcp/dist/parsers/security.js +16 -0
- package/{index.js → mcp/dist/server.js} +408 -4
- package/package.json +19 -15
- package/scripts/sfn-pr-check.js +108 -0
- package/scripts/sfn-pr-check.ts +121 -0
- package/scripts/utils.js +29 -0
- package/scripts/utils.ts +31 -0
- package/.secufusion-project-spec.json +0 -1068
- /package/commands/{code.md → sfn-code.md} +0 -0
- /package/commands/{init.md → sfn-init.md} +0 -0
- /package/commands/{plan.md → sfn-plan.md} +0 -0
- /package/commands/{review.md → sfn-review.md} +0 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export function parseDatabase(filePath, fileContent, repository) {
|
|
2
|
+
const entities = [];
|
|
3
|
+
if (filePath.endsWith('.sql')) {
|
|
4
|
+
const tableRegex = /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?([^\s\(]+)/gi;
|
|
5
|
+
let match;
|
|
6
|
+
while ((match = tableRegex.exec(fileContent)) !== null) {
|
|
7
|
+
entities.push({
|
|
8
|
+
tableName: match[1],
|
|
9
|
+
filePath,
|
|
10
|
+
repository,
|
|
11
|
+
schemaType: 'SQL'
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
else if (filePath.endsWith('.prisma')) {
|
|
16
|
+
const modelRegex = /model\s+(\w+)\s+\{/g;
|
|
17
|
+
let match;
|
|
18
|
+
while ((match = modelRegex.exec(fileContent)) !== null) {
|
|
19
|
+
entities.push({
|
|
20
|
+
tableName: match[1],
|
|
21
|
+
filePath,
|
|
22
|
+
repository,
|
|
23
|
+
schemaType: 'Prisma'
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
else if (fileContent.includes('new mongoose.Schema')) {
|
|
28
|
+
const modelRegex = /mongoose\.model\s*\(\s*['"]([^'"]+)['"]/g;
|
|
29
|
+
let match;
|
|
30
|
+
while ((match = modelRegex.exec(fileContent)) !== null) {
|
|
31
|
+
entities.push({
|
|
32
|
+
tableName: match[1],
|
|
33
|
+
filePath,
|
|
34
|
+
repository,
|
|
35
|
+
schemaType: 'Mongoose'
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return entities;
|
|
40
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function parseDomainModels(filePath, fileContent, repository) {
|
|
2
|
+
const models = [];
|
|
3
|
+
const classMatch = fileContent.match(/public\s+class\s+(\w+)/);
|
|
4
|
+
if (!classMatch)
|
|
5
|
+
return models;
|
|
6
|
+
const className = classMatch[1];
|
|
7
|
+
const violations = [];
|
|
8
|
+
if (fileContent.includes("@Entity")) {
|
|
9
|
+
if (!fileContent.includes("tenantId") && !fileContent.includes("TenantId")) {
|
|
10
|
+
violations.push("P1 Violation: Entity is missing 'tenantId' field for tenant isolation.");
|
|
11
|
+
}
|
|
12
|
+
models.push({ name: className, type: "Entity", filePath, repository, violations });
|
|
13
|
+
}
|
|
14
|
+
if (className.endsWith("DTO") || className.endsWith("Dto") || className.endsWith("Request") || className.endsWith("Response")) {
|
|
15
|
+
if (!fileContent.includes("@Data") || !fileContent.includes("@Builder")) {
|
|
16
|
+
violations.push("Warning: DTO is missing @Data or @Builder Lombok annotations.");
|
|
17
|
+
}
|
|
18
|
+
models.push({ name: className, type: "DTO", filePath, repository, violations });
|
|
19
|
+
}
|
|
20
|
+
return models;
|
|
21
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export function parseEventTopics(filePath, fileContent, repository) {
|
|
2
|
+
const events = [];
|
|
3
|
+
const classMatch = fileContent.match(/public\s+class\s+(\w+)/);
|
|
4
|
+
if (!classMatch)
|
|
5
|
+
return events;
|
|
6
|
+
const className = classMatch[1];
|
|
7
|
+
const topicsConsumed = [];
|
|
8
|
+
const topicsProduced = [];
|
|
9
|
+
const violations = [];
|
|
10
|
+
const listenerRegex = /@KafkaListener\s*\(\s*topics\s*=\s*["']([^"']+)["']/g;
|
|
11
|
+
let match;
|
|
12
|
+
while ((match = listenerRegex.exec(fileContent)) !== null) {
|
|
13
|
+
topicsConsumed.push(match[1]);
|
|
14
|
+
}
|
|
15
|
+
const producerRegex = /KafkaTemplate.*\.send\(\s*["']([^"']+)["']/g;
|
|
16
|
+
while ((match = producerRegex.exec(fileContent)) !== null) {
|
|
17
|
+
topicsProduced.push(match[1]);
|
|
18
|
+
}
|
|
19
|
+
if (topicsConsumed.length === 0 && topicsProduced.length === 0) {
|
|
20
|
+
return events;
|
|
21
|
+
}
|
|
22
|
+
if (topicsConsumed.length > 0) {
|
|
23
|
+
if (fileContent.includes("RestTemplate") || fileContent.includes("FeignClient") || fileContent.includes("HttpClient")) {
|
|
24
|
+
violations.push("P4 Violation (KAFKA_SYNC): Potential synchronous blocking call detected inside or near @KafkaListener.");
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
events.push({
|
|
28
|
+
className,
|
|
29
|
+
topicsConsumed,
|
|
30
|
+
topicsProduced,
|
|
31
|
+
filePath,
|
|
32
|
+
repository,
|
|
33
|
+
violations
|
|
34
|
+
});
|
|
35
|
+
return events;
|
|
36
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export function parseFrontend(filePath, fileContent, repository) {
|
|
2
|
+
const components = [];
|
|
3
|
+
if (filePath.endsWith('.tsx') || filePath.endsWith('.jsx')) {
|
|
4
|
+
const match = fileContent.match(/(?:export\s+)?(?:default\s+)?(?:function|const)\s+([A-Z]\w+)/);
|
|
5
|
+
if (match) {
|
|
6
|
+
components.push({
|
|
7
|
+
componentName: match[1],
|
|
8
|
+
type: 'React',
|
|
9
|
+
filePath,
|
|
10
|
+
repository
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
else if (filePath.endsWith('.vue')) {
|
|
15
|
+
components.push({
|
|
16
|
+
componentName: filePath.split('/').pop()?.replace('.vue', '') || 'VueComponent',
|
|
17
|
+
type: 'Vue',
|
|
18
|
+
filePath,
|
|
19
|
+
repository
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
else if (filePath.endsWith('.ts') && fileContent.includes('@Component')) {
|
|
23
|
+
const match = fileContent.match(/export\s+class\s+(\w+Component)/);
|
|
24
|
+
if (match) {
|
|
25
|
+
components.push({
|
|
26
|
+
componentName: match[1],
|
|
27
|
+
type: 'Angular',
|
|
28
|
+
filePath,
|
|
29
|
+
repository
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return components;
|
|
34
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export function parseInfra(filePath, fileContent, repository) {
|
|
2
|
+
const resources = [];
|
|
3
|
+
if (filePath.endsWith('docker-compose.yml') || filePath.endsWith('docker-compose.yaml')) {
|
|
4
|
+
const servicesRegex = /services:\s*\n((\s+[\w-]+:\s*\n)+)/;
|
|
5
|
+
const match = fileContent.match(servicesRegex);
|
|
6
|
+
if (match) {
|
|
7
|
+
resources.push({
|
|
8
|
+
type: 'DockerCompose',
|
|
9
|
+
name: 'Compose-Services',
|
|
10
|
+
filePath,
|
|
11
|
+
repository
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
else if (filePath.endsWith('.yaml') || filePath.endsWith('.yml')) {
|
|
16
|
+
if (fileContent.includes('apiVersion:') && fileContent.includes('kind:')) {
|
|
17
|
+
const kindMatch = fileContent.match(/kind:\s*(\w+)/);
|
|
18
|
+
const nameMatch = fileContent.match(/name:\s*([^\n]+)/);
|
|
19
|
+
if (kindMatch && nameMatch) {
|
|
20
|
+
resources.push({
|
|
21
|
+
type: `Kubernetes-${kindMatch[1]}`,
|
|
22
|
+
name: nameMatch[1],
|
|
23
|
+
filePath,
|
|
24
|
+
repository
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
else if (filePath.endsWith('.tf')) {
|
|
30
|
+
const resourceRegex = /resource\s+"([^"]+)"\s+"([^"]+)"/g;
|
|
31
|
+
let match;
|
|
32
|
+
while ((match = resourceRegex.exec(fileContent)) !== null) {
|
|
33
|
+
resources.push({
|
|
34
|
+
type: `Terraform-${match[1]}`,
|
|
35
|
+
name: match[2],
|
|
36
|
+
filePath,
|
|
37
|
+
repository
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return resources;
|
|
42
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export function inferPatterns(dna) {
|
|
2
|
+
const inferredPatterns = [];
|
|
3
|
+
// Infer DTO naming patterns
|
|
4
|
+
const domains = dna.domains || [];
|
|
5
|
+
if (domains.length > 0) {
|
|
6
|
+
const dtos = domains.filter((d) => d.type === "DTO");
|
|
7
|
+
if (dtos.length > 0) {
|
|
8
|
+
let suffixDTO = 0;
|
|
9
|
+
let suffixRequest = 0;
|
|
10
|
+
let suffixResponse = 0;
|
|
11
|
+
dtos.forEach((d) => {
|
|
12
|
+
if (d.name.endsWith("DTO"))
|
|
13
|
+
suffixDTO++;
|
|
14
|
+
else if (d.name.endsWith("Request"))
|
|
15
|
+
suffixRequest++;
|
|
16
|
+
else if (d.name.endsWith("Response"))
|
|
17
|
+
suffixResponse++;
|
|
18
|
+
});
|
|
19
|
+
const max = Math.max(suffixDTO, suffixRequest, suffixResponse);
|
|
20
|
+
const total = dtos.length;
|
|
21
|
+
if (max / total > 0.7) {
|
|
22
|
+
if (max === suffixDTO)
|
|
23
|
+
inferredPatterns.push("Inferred Pattern: 70%+ of data models use 'DTO' suffix.");
|
|
24
|
+
if (max === suffixRequest)
|
|
25
|
+
inferredPatterns.push("Inferred Pattern: 70%+ of data models use 'Request' suffix.");
|
|
26
|
+
if (max === suffixResponse)
|
|
27
|
+
inferredPatterns.push("Inferred Pattern: 70%+ of data models use 'Response' suffix.");
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// Infer API Path Versioning
|
|
32
|
+
const apis = dna.apis || [];
|
|
33
|
+
if (apis.length > 0) {
|
|
34
|
+
let versioned = 0;
|
|
35
|
+
apis.forEach((a) => {
|
|
36
|
+
const paths = a.endpoints.join(" ");
|
|
37
|
+
if (paths.includes("/v1/") || paths.includes("/v2/")) {
|
|
38
|
+
versioned++;
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
if (versioned / apis.length < 0.1) {
|
|
42
|
+
inferredPatterns.push("Inferred Pattern: Codebase does not heavily use URL path versioning (e.g., /v1/).");
|
|
43
|
+
}
|
|
44
|
+
else if (versioned / apis.length > 0.8) {
|
|
45
|
+
inferredPatterns.push("Inferred Pattern: Codebase strictly enforces URL path versioning (e.g., /v1/).");
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return inferredPatterns;
|
|
49
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function parseSecurity(filePath, fileContent, repository) {
|
|
2
|
+
const contexts = [];
|
|
3
|
+
if (fileContent.includes('KeycloakWebSecurityConfigurerAdapter') || fileContent.includes('keycloak.json')) {
|
|
4
|
+
contexts.push({ authType: 'Keycloak', filePath, repository });
|
|
5
|
+
}
|
|
6
|
+
if (fileContent.includes('@PreAuthorize') || fileContent.includes('@RolesAllowed')) {
|
|
7
|
+
contexts.push({ authType: 'SpringSecurity-RBAC', filePath, repository });
|
|
8
|
+
}
|
|
9
|
+
if (fileContent.includes('jwt') || fileContent.includes('jsonwebtoken') || fileContent.includes('JwtDecoder')) {
|
|
10
|
+
contexts.push({ authType: 'JWT', filePath, repository });
|
|
11
|
+
}
|
|
12
|
+
if (fileContent.includes('passport.use')) {
|
|
13
|
+
contexts.push({ authType: 'PassportJS', filePath, repository });
|
|
14
|
+
}
|
|
15
|
+
return contexts;
|
|
16
|
+
}
|