@skaleagents/swarm 0.4.0 → 0.6.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/README.md +95 -3
- package/dist/application/graph.d.ts +45 -0
- package/dist/application/graph.js +264 -0
- package/dist/application/intake.d.ts +63 -0
- package/dist/application/intake.js +182 -0
- package/dist/application/review.d.ts +71 -0
- package/dist/application/review.js +360 -0
- package/dist/application/tools.d.ts +2 -0
- package/dist/application/tools.js +94 -0
- package/dist/iac/parse.d.ts +25 -0
- package/dist/iac/parse.js +239 -0
- package/dist/iac/rules.d.ts +16 -0
- package/dist/iac/rules.js +450 -0
- package/dist/iac/scan.d.ts +43 -0
- package/dist/iac/scan.js +60 -0
- package/dist/review.d.ts +11 -1
- package/dist/review.js +72 -11
- package/dist/server.js +118 -68
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -0
- package/package.json +6 -4
package/dist/review.js
CHANGED
|
@@ -16,14 +16,16 @@ export function architectureFindings(content, focus) {
|
|
|
16
16
|
detail: `Checked ${content.length} characters with the ${focusValue} ruleset. Findings are pattern-based and should be validated against the running system.`,
|
|
17
17
|
},
|
|
18
18
|
];
|
|
19
|
-
if (shouldInclude(focusValue, "security") &&
|
|
19
|
+
if (shouldInclude(focusValue, "security") &&
|
|
20
|
+
/0\.0\.0\.0(?:\/0)?/.test(content)) {
|
|
20
21
|
findings.push({
|
|
21
22
|
severity: "high",
|
|
22
23
|
title: "Broad network exposure",
|
|
23
24
|
detail: "Detected a possible wide-open CIDR or bind address. Restrict ingress to approved sources and keep public listeners behind the intended edge control.",
|
|
24
25
|
});
|
|
25
26
|
}
|
|
26
|
-
if (shouldInclude(focusValue, "security") &&
|
|
27
|
+
if (shouldInclude(focusValue, "security") &&
|
|
28
|
+
/AKIA[0-9A-Z]{16}/.test(content)) {
|
|
27
29
|
findings.push({
|
|
28
30
|
severity: "critical",
|
|
29
31
|
title: "Possible AWS access key",
|
|
@@ -39,8 +41,9 @@ export function architectureFindings(content, focus) {
|
|
|
39
41
|
});
|
|
40
42
|
}
|
|
41
43
|
if (shouldInclude(focusValue, "security") &&
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
[
|
|
45
|
+
...content.matchAll(/(?:password|passwd|secret|api[_-]?key|auth[_-]?token)["']?\s*[:=]\s*["']([^"'\n]+)["']/gi),
|
|
46
|
+
].some((match) => literalCredential(match[1]))) {
|
|
44
47
|
findings.push({
|
|
45
48
|
severity: "high",
|
|
46
49
|
title: "Hardcoded credential-like value",
|
|
@@ -64,14 +67,15 @@ export function architectureFindings(content, focus) {
|
|
|
64
67
|
});
|
|
65
68
|
}
|
|
66
69
|
if (shouldInclude(focusValue, "security") &&
|
|
67
|
-
|
|
70
|
+
[...content.matchAll(/\bhttp:\/\/[^\s"'<>]+/gi)].some((match) => externalHttp(match[0]))) {
|
|
68
71
|
findings.push({
|
|
69
72
|
severity: "medium",
|
|
70
73
|
title: "Unencrypted HTTP endpoint",
|
|
71
74
|
detail: "Detected an HTTP URL outside localhost. Use HTTPS for service and dependency traffic, and verify certificate validation is enabled.",
|
|
72
75
|
});
|
|
73
76
|
}
|
|
74
|
-
if ((shouldInclude(focusValue, "security") ||
|
|
77
|
+
if ((shouldInclude(focusValue, "security") ||
|
|
78
|
+
shouldInclude(focusValue, "reliability")) &&
|
|
75
79
|
/(?:^|[\s:=])(?:[\w./-]+:)?latest(?:[\s"']|$)/im.test(content)) {
|
|
76
80
|
findings.push({
|
|
77
81
|
severity: "medium",
|
|
@@ -95,14 +99,74 @@ export function architectureFindings(content, focus) {
|
|
|
95
99
|
detail: "The content enables debug mode. Disable it in production to avoid noisy behavior and accidental disclosure of internal details.",
|
|
96
100
|
});
|
|
97
101
|
}
|
|
98
|
-
if (shouldInclude(focusValue, "cost") &&
|
|
102
|
+
if (shouldInclude(focusValue, "cost") &&
|
|
103
|
+
/(?:instance_type|machine_type|vm_size)\s*[:=]/i.test(content)) {
|
|
99
104
|
findings.push({
|
|
100
105
|
severity: "info",
|
|
101
106
|
title: "Compute sizing needs review",
|
|
102
107
|
detail: "Detected an explicit compute size. Compare the selected size with observed utilization and set a review point for scale-up and scale-down decisions.",
|
|
103
108
|
});
|
|
104
109
|
}
|
|
105
|
-
return findings
|
|
110
|
+
return findings.map((finding, index) => {
|
|
111
|
+
if (index === 0)
|
|
112
|
+
return finding;
|
|
113
|
+
const matchers = {
|
|
114
|
+
"Broad network exposure": /0\.0\.0\.0(?:\/0)?/,
|
|
115
|
+
"Possible AWS access key": /AKIA[0-9A-Z]{16}/,
|
|
116
|
+
"Private key material in source": /-----BEGIN (?:RSA |EC |OPENSSH |DSA |)PRIVATE KEY-----/,
|
|
117
|
+
"Hardcoded credential-like value": /(?:password|passwd|secret|api[_-]?key|auth[_-]?token)["']?\s*[:=]\s*["']([^"'\n]+)["']/gi,
|
|
118
|
+
"Wildcard permission detected": /(?:Action|actions?)\s*[:=][^\n]*["']?\*["']?/i,
|
|
119
|
+
"Elevated container privileges": /(?:privileged|hostNetwork|allowPrivilegeEscalation)\s*:\s*true/i,
|
|
120
|
+
"Unencrypted HTTP endpoint": /\bhttp:\/\/[^\s"'<>]+/gi,
|
|
121
|
+
"Unpinned container image": /\blatest\b/,
|
|
122
|
+
"Public data access pattern": /(?:public-read|publicRead|allUsers)/i,
|
|
123
|
+
"Debug mode enabled": /(?:debug|app_debug)\s*[:=]\s*["']?(?:true|1|yes)/i,
|
|
124
|
+
"Compute sizing needs review": /(?:instance_type|machine_type|vm_size)\s*[:=]/i,
|
|
125
|
+
};
|
|
126
|
+
const pattern = matchers[finding.title];
|
|
127
|
+
const matches = pattern
|
|
128
|
+
? [
|
|
129
|
+
...content.matchAll(new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : pattern.flags + "g")),
|
|
130
|
+
]
|
|
131
|
+
: [];
|
|
132
|
+
const match = matches.find((m) => finding.title === "Hardcoded credential-like value"
|
|
133
|
+
? literalCredential(m[1])
|
|
134
|
+
: finding.title === "Unencrypted HTTP endpoint"
|
|
135
|
+
? externalHttp(m[0])
|
|
136
|
+
: true);
|
|
137
|
+
const offset = match?.index ?? 0;
|
|
138
|
+
const before = content.slice(0, offset);
|
|
139
|
+
const ruleIndex = Object.keys(matchers).indexOf(finding.title) + 1;
|
|
140
|
+
return {
|
|
141
|
+
...finding,
|
|
142
|
+
ruleId: `APP${String(ruleIndex).padStart(3, "0")}`,
|
|
143
|
+
category: (finding.title === "Compute sizing needs review"
|
|
144
|
+
? "cost"
|
|
145
|
+
: ["Debug mode enabled", "Unpinned container image"].includes(finding.title)
|
|
146
|
+
? "reliability"
|
|
147
|
+
: "security"),
|
|
148
|
+
remediation: finding.detail,
|
|
149
|
+
location: {
|
|
150
|
+
line: before.split("\n").length,
|
|
151
|
+
column: offset - before.lastIndexOf("\n"),
|
|
152
|
+
path: "source",
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
export function literalCredential(value) {
|
|
158
|
+
return (value.trim().length > 0 &&
|
|
159
|
+
!/^(?:\$\{|\{\{|<[^>]+>$|process\.env\.|var\.|secrets?\.)/.test(value));
|
|
160
|
+
}
|
|
161
|
+
export function externalHttp(value) {
|
|
162
|
+
try {
|
|
163
|
+
const url = new URL(value);
|
|
164
|
+
return (url.protocol === "http:" &&
|
|
165
|
+
!["localhost", "127.0.0.1", "[::1]"].includes(url.hostname));
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
106
170
|
}
|
|
107
171
|
export async function fetchPublicBotHints(token) {
|
|
108
172
|
try {
|
|
@@ -125,6 +189,3 @@ export async function fetchPublicBotHints(token) {
|
|
|
125
189
|
return [];
|
|
126
190
|
}
|
|
127
191
|
}
|
|
128
|
-
export function countIacResources(content) {
|
|
129
|
-
return (content.match(/\bresource\b|\bkind:\s*\w+/gi) ?? []).length;
|
|
130
|
-
}
|
package/dist/server.js
CHANGED
|
@@ -1,15 +1,62 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { getApiAccessToken, requireApiAuth, unauthorizedContent, } from "./auth.js";
|
|
4
|
-
import { architectureFindings,
|
|
4
|
+
import { architectureFindings, fetchPublicBotHints } from "./review.js";
|
|
5
|
+
import { looksLikeIac, ScanInputError } from "./iac/parse.js";
|
|
6
|
+
import { filterFindings, scanIac, summarize } from "./iac/scan.js";
|
|
7
|
+
import { registerConsultation } from "./application/tools.js";
|
|
8
|
+
import { VERSION } from "./version.js";
|
|
9
|
+
const contentSchema = z
|
|
10
|
+
.string()
|
|
11
|
+
.min(1)
|
|
12
|
+
.max(500_000)
|
|
13
|
+
.refine((value) => value.trim().length > 0, "Content must not be whitespace");
|
|
14
|
+
const filters = {
|
|
15
|
+
focus: z
|
|
16
|
+
.enum(["security", "reliability", "cost", "general"])
|
|
17
|
+
.default("general"),
|
|
18
|
+
minSeverity: z
|
|
19
|
+
.enum(["info", "low", "medium", "high", "critical"])
|
|
20
|
+
.default("info")
|
|
21
|
+
.describe("Lowest finding severity to return"),
|
|
22
|
+
maxFindings: z
|
|
23
|
+
.number()
|
|
24
|
+
.int()
|
|
25
|
+
.min(1)
|
|
26
|
+
.max(500)
|
|
27
|
+
.default(100)
|
|
28
|
+
.describe("Maximum findings returned; totals include all matching findings"),
|
|
29
|
+
};
|
|
30
|
+
function result(output) {
|
|
31
|
+
return {
|
|
32
|
+
content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
|
|
33
|
+
structuredContent: output,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function inputError(error) {
|
|
37
|
+
if (!(error instanceof ScanInputError))
|
|
38
|
+
throw error;
|
|
39
|
+
return {
|
|
40
|
+
isError: true,
|
|
41
|
+
content: [
|
|
42
|
+
{
|
|
43
|
+
type: "text",
|
|
44
|
+
text: JSON.stringify({
|
|
45
|
+
error: "invalid_input",
|
|
46
|
+
message: error.message,
|
|
47
|
+
}),
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
};
|
|
51
|
+
}
|
|
5
52
|
export function createServer(remote = false) {
|
|
6
|
-
const server = new McpServer({
|
|
7
|
-
|
|
8
|
-
version: "0.4.0",
|
|
53
|
+
const server = new McpServer({ name: "skaleagents-swarm", version: VERSION }, {
|
|
54
|
+
instructions: "For a whole application architecture consultation, start with plan_architecture_review, read relevant files with the client's workspace tools, then call review_application_architecture. Ask its follow-up questions and carry answers forward in context. Use review_architecture for individual source snippets and scan_iac for infrastructure. Ground conclusions in the returned evidence and coverage limits.",
|
|
9
55
|
});
|
|
56
|
+
registerConsultation(server, remote);
|
|
10
57
|
server.registerTool("review_architecture", {
|
|
11
58
|
title: "Review architecture",
|
|
12
|
-
description: "Review
|
|
59
|
+
description: "Review individual source snippets or parsed infrastructure for security, reliability, and cost risks. For a whole application or Next.js architecture consultation, use plan_architecture_review and review_application_architecture instead.",
|
|
13
60
|
annotations: {
|
|
14
61
|
readOnlyHint: true,
|
|
15
62
|
destructiveHint: false,
|
|
@@ -17,16 +64,8 @@ export function createServer(remote = false) {
|
|
|
17
64
|
},
|
|
18
65
|
_meta: { securitySchemes: [{ type: "oauth2", scopes: ["mcp"] }] },
|
|
19
66
|
inputSchema: {
|
|
20
|
-
content:
|
|
21
|
-
|
|
22
|
-
.min(1)
|
|
23
|
-
.max(500_000)
|
|
24
|
-
.describe("Application source or IaC text to review"),
|
|
25
|
-
focus: z
|
|
26
|
-
.enum(["security", "reliability", "cost", "general"])
|
|
27
|
-
.optional()
|
|
28
|
-
.default("general")
|
|
29
|
-
.describe("Review focus: security, reliability, cost, general"),
|
|
67
|
+
content: contentSchema.describe("Application source or IaC text to review"),
|
|
68
|
+
...filters,
|
|
30
69
|
format: z
|
|
31
70
|
.enum([
|
|
32
71
|
"terraform",
|
|
@@ -39,7 +78,7 @@ export function createServer(remote = false) {
|
|
|
39
78
|
.default("auto")
|
|
40
79
|
.describe("Content format: terraform, cloudformation, kubernetes, application, auto"),
|
|
41
80
|
},
|
|
42
|
-
}, async ({ content, focus, format }) => {
|
|
81
|
+
}, async ({ content, focus, format, minSeverity, maxFindings }) => {
|
|
43
82
|
let token;
|
|
44
83
|
if (!remote) {
|
|
45
84
|
const auth = await requireApiAuth();
|
|
@@ -49,59 +88,70 @@ export function createServer(remote = false) {
|
|
|
49
88
|
if (!token)
|
|
50
89
|
return unauthorizedContent({ ok: false, reason: "oauth_failed" });
|
|
51
90
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
.string()
|
|
81
|
-
.optional()
|
|
82
|
-
.describe("terraform, cloudformation, kubernetes, auto"),
|
|
83
|
-
},
|
|
84
|
-
}, async ({ content, format }) => {
|
|
85
|
-
if (!remote) {
|
|
86
|
-
const auth = await requireApiAuth();
|
|
87
|
-
if (!auth.ok)
|
|
88
|
-
return unauthorizedContent(auth);
|
|
91
|
+
try {
|
|
92
|
+
const options = { focus, minSeverity, maxFindings };
|
|
93
|
+
if (format !== "application" &&
|
|
94
|
+
(format !== "auto" || looksLikeIac(content))) {
|
|
95
|
+
return result({
|
|
96
|
+
...scanIac(content, { ...options, format }),
|
|
97
|
+
botHints: await fetchPublicBotHints(token),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
const findings = filterFindings(architectureFindings(content, focus).slice(1), options);
|
|
101
|
+
return result({
|
|
102
|
+
status: "completed",
|
|
103
|
+
engineVersion: VERSION,
|
|
104
|
+
format: "application",
|
|
105
|
+
focus,
|
|
106
|
+
summary: `Application review completed; ${findings.length} findings match the selected filters.`,
|
|
107
|
+
findings: findings.slice(0, maxFindings),
|
|
108
|
+
totalFindings: findings.length,
|
|
109
|
+
totals: summarize(findings),
|
|
110
|
+
truncated: findings.length > maxFindings,
|
|
111
|
+
limitations: [
|
|
112
|
+
"Application checks are text patterns, not a language-aware or runtime analysis. Findings do not establish that code is safe.",
|
|
113
|
+
],
|
|
114
|
+
botHints: await fetchPublicBotHints(token),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
return inputError(error);
|
|
89
119
|
}
|
|
90
|
-
const formatValue = format === "terraform" ||
|
|
91
|
-
format === "cloudformation" ||
|
|
92
|
-
format === "kubernetes" ||
|
|
93
|
-
format === "auto"
|
|
94
|
-
? format
|
|
95
|
-
: "auto";
|
|
96
|
-
const output = {
|
|
97
|
-
status: "stub",
|
|
98
|
-
message: "Full IaC scanning lands in Phase 2 agent-swarm",
|
|
99
|
-
format: formatValue,
|
|
100
|
-
parsedResourceCount: countIacResources(content),
|
|
101
|
-
};
|
|
102
|
-
return {
|
|
103
|
-
content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
|
|
104
|
-
};
|
|
105
120
|
});
|
|
121
|
+
for (const name of ["scan_iac", "scan_iac_stub"])
|
|
122
|
+
server.registerTool(name, {
|
|
123
|
+
title: name === "scan_iac"
|
|
124
|
+
? "Scan infrastructure"
|
|
125
|
+
: "Scan infrastructure (compatibility alias)",
|
|
126
|
+
description: (name === "scan_iac_stub"
|
|
127
|
+
? "Compatibility alias for scan_iac; runs the full scanner. "
|
|
128
|
+
: "") +
|
|
129
|
+
"Parse Terraform HCL/JSON, CloudFormation YAML/JSON, or Kubernetes manifests. Check security, reliability and cost rules with resource locations and remediation.",
|
|
130
|
+
annotations: {
|
|
131
|
+
readOnlyHint: true,
|
|
132
|
+
destructiveHint: false,
|
|
133
|
+
openWorldHint: false,
|
|
134
|
+
},
|
|
135
|
+
_meta: { securitySchemes: [{ type: "oauth2", scopes: ["mcp"] }] },
|
|
136
|
+
inputSchema: {
|
|
137
|
+
content: contentSchema.describe("Terraform HCL/JSON, CloudFormation YAML/JSON, or Kubernetes YAML/JSON"),
|
|
138
|
+
format: z
|
|
139
|
+
.enum(["terraform", "cloudformation", "kubernetes", "auto"])
|
|
140
|
+
.default("auto"),
|
|
141
|
+
...filters,
|
|
142
|
+
},
|
|
143
|
+
}, async ({ content, format, focus, minSeverity, maxFindings }) => {
|
|
144
|
+
if (!remote) {
|
|
145
|
+
const auth = await requireApiAuth();
|
|
146
|
+
if (!auth.ok)
|
|
147
|
+
return unauthorizedContent(auth);
|
|
148
|
+
}
|
|
149
|
+
try {
|
|
150
|
+
return result(scanIac(content, { format, focus, minSeverity, maxFindings }));
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
return inputError(error);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
106
156
|
return server;
|
|
107
157
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const VERSION = "0.6.0";
|
package/dist/version.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const VERSION = "0.6.0";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skaleagents/swarm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"start": "node dist/index.js",
|
|
25
25
|
"dev": "tsx src/index.ts",
|
|
26
26
|
"typecheck": "tsc --noEmit",
|
|
27
|
-
"test": "tsx --test src
|
|
27
|
+
"test": "tsx --test src/*.test.ts src/iac/*.test.ts src/application/*.test.ts",
|
|
28
28
|
"smoke": "tsx scripts/smoke-tools.mjs"
|
|
29
29
|
},
|
|
30
30
|
"engines": {
|
|
@@ -32,11 +32,13 @@
|
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
35
|
+
"hcl2-parser": "^1.0.3",
|
|
36
|
+
"typescript": "^5.9.3",
|
|
37
|
+
"yaml": "^2.9.1",
|
|
35
38
|
"zod": "^4.4.3"
|
|
36
39
|
},
|
|
37
40
|
"devDependencies": {
|
|
38
41
|
"@types/node": "^22.20.1",
|
|
39
|
-
"tsx": "^4.23.12"
|
|
40
|
-
"typescript": "^5.7.3"
|
|
42
|
+
"tsx": "^4.23.12"
|
|
41
43
|
}
|
|
42
44
|
}
|