@skaleagents/swarm 0.3.1 → 0.5.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 +66 -6
- package/dist/http.d.ts +2 -0
- package/dist/http.js +134 -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 +59 -0
- package/dist/index.js +5 -87
- package/dist/review.d.ts +12 -2
- package/dist/review.js +74 -12
- package/dist/server.d.ts +2 -0
- package/dist/server.js +155 -0
- package/package.json +11 -3
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,21 +99,82 @@ 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 {
|
|
109
173
|
const res = await fetch(`${getApiUrl()}/api/bots`, {
|
|
174
|
+
signal: AbortSignal.timeout(10_000),
|
|
110
175
|
headers: {
|
|
111
176
|
Accept: "application/json",
|
|
112
|
-
Authorization: `Bearer ${token}
|
|
177
|
+
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
|
113
178
|
},
|
|
114
179
|
});
|
|
115
180
|
if (!res.ok)
|
|
@@ -124,6 +189,3 @@ export async function fetchPublicBotHints(token) {
|
|
|
124
189
|
return [];
|
|
125
190
|
}
|
|
126
191
|
}
|
|
127
|
-
export function countIacResources(content) {
|
|
128
|
-
return (content.match(/\bresource\b|\bkind:\s*\w+/gi) ?? []).length;
|
|
129
|
-
}
|
package/dist/server.d.ts
ADDED
package/dist/server.js
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { getApiAccessToken, requireApiAuth, unauthorizedContent, } from "./auth.js";
|
|
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
|
+
const contentSchema = z
|
|
8
|
+
.string()
|
|
9
|
+
.min(1)
|
|
10
|
+
.max(500_000)
|
|
11
|
+
.refine((value) => value.trim().length > 0, "Content must not be whitespace");
|
|
12
|
+
const filters = {
|
|
13
|
+
focus: z
|
|
14
|
+
.enum(["security", "reliability", "cost", "general"])
|
|
15
|
+
.default("general"),
|
|
16
|
+
minSeverity: z
|
|
17
|
+
.enum(["info", "low", "medium", "high", "critical"])
|
|
18
|
+
.default("info")
|
|
19
|
+
.describe("Lowest finding severity to return"),
|
|
20
|
+
maxFindings: z
|
|
21
|
+
.number()
|
|
22
|
+
.int()
|
|
23
|
+
.min(1)
|
|
24
|
+
.max(500)
|
|
25
|
+
.default(100)
|
|
26
|
+
.describe("Maximum findings returned; totals include all matching findings"),
|
|
27
|
+
};
|
|
28
|
+
function result(output) {
|
|
29
|
+
return {
|
|
30
|
+
content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
|
|
31
|
+
structuredContent: output,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
function inputError(error) {
|
|
35
|
+
if (!(error instanceof ScanInputError))
|
|
36
|
+
throw error;
|
|
37
|
+
return {
|
|
38
|
+
isError: true,
|
|
39
|
+
content: [
|
|
40
|
+
{
|
|
41
|
+
type: "text",
|
|
42
|
+
text: JSON.stringify({
|
|
43
|
+
error: "invalid_input",
|
|
44
|
+
message: error.message,
|
|
45
|
+
}),
|
|
46
|
+
},
|
|
47
|
+
],
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export function createServer(remote = false) {
|
|
51
|
+
const server = new McpServer({
|
|
52
|
+
name: "skaleagents-swarm",
|
|
53
|
+
version: "0.5.0",
|
|
54
|
+
});
|
|
55
|
+
server.registerTool("review_architecture", {
|
|
56
|
+
title: "Review architecture",
|
|
57
|
+
description: "Review application source or parsed infrastructure for security, reliability, and cost risks. Returns located findings, rule IDs, remediation and coverage limits.",
|
|
58
|
+
annotations: {
|
|
59
|
+
readOnlyHint: true,
|
|
60
|
+
destructiveHint: false,
|
|
61
|
+
openWorldHint: false,
|
|
62
|
+
},
|
|
63
|
+
_meta: { securitySchemes: [{ type: "oauth2", scopes: ["mcp"] }] },
|
|
64
|
+
inputSchema: {
|
|
65
|
+
content: contentSchema.describe("Application source or IaC text to review"),
|
|
66
|
+
...filters,
|
|
67
|
+
format: z
|
|
68
|
+
.enum([
|
|
69
|
+
"terraform",
|
|
70
|
+
"cloudformation",
|
|
71
|
+
"kubernetes",
|
|
72
|
+
"application",
|
|
73
|
+
"auto",
|
|
74
|
+
])
|
|
75
|
+
.optional()
|
|
76
|
+
.default("auto")
|
|
77
|
+
.describe("Content format: terraform, cloudformation, kubernetes, application, auto"),
|
|
78
|
+
},
|
|
79
|
+
}, async ({ content, focus, format, minSeverity, maxFindings }) => {
|
|
80
|
+
let token;
|
|
81
|
+
if (!remote) {
|
|
82
|
+
const auth = await requireApiAuth();
|
|
83
|
+
if (!auth.ok)
|
|
84
|
+
return unauthorizedContent(auth);
|
|
85
|
+
token = (await getApiAccessToken()) ?? undefined;
|
|
86
|
+
if (!token)
|
|
87
|
+
return unauthorizedContent({ ok: false, reason: "oauth_failed" });
|
|
88
|
+
}
|
|
89
|
+
try {
|
|
90
|
+
const options = { focus, minSeverity, maxFindings };
|
|
91
|
+
if (format !== "application" &&
|
|
92
|
+
(format !== "auto" || looksLikeIac(content))) {
|
|
93
|
+
return result({
|
|
94
|
+
...scanIac(content, { ...options, format }),
|
|
95
|
+
botHints: await fetchPublicBotHints(token),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
const findings = filterFindings(architectureFindings(content, focus).slice(1), options);
|
|
99
|
+
return result({
|
|
100
|
+
status: "completed",
|
|
101
|
+
engineVersion: "0.5.0",
|
|
102
|
+
format: "application",
|
|
103
|
+
focus,
|
|
104
|
+
summary: `Application review completed; ${findings.length} findings match the selected filters.`,
|
|
105
|
+
findings: findings.slice(0, maxFindings),
|
|
106
|
+
totalFindings: findings.length,
|
|
107
|
+
totals: summarize(findings),
|
|
108
|
+
truncated: findings.length > maxFindings,
|
|
109
|
+
limitations: [
|
|
110
|
+
"Application checks are text patterns, not a language-aware or runtime analysis. Findings do not establish that code is safe.",
|
|
111
|
+
],
|
|
112
|
+
botHints: await fetchPublicBotHints(token),
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
catch (error) {
|
|
116
|
+
return inputError(error);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
for (const name of ["scan_iac", "scan_iac_stub"])
|
|
120
|
+
server.registerTool(name, {
|
|
121
|
+
title: name === "scan_iac"
|
|
122
|
+
? "Scan infrastructure"
|
|
123
|
+
: "Scan infrastructure (compatibility alias)",
|
|
124
|
+
description: (name === "scan_iac_stub"
|
|
125
|
+
? "Compatibility alias for scan_iac; runs the full scanner. "
|
|
126
|
+
: "") +
|
|
127
|
+
"Parse Terraform HCL/JSON, CloudFormation YAML/JSON, or Kubernetes manifests. Check security, reliability and cost rules with resource locations and remediation.",
|
|
128
|
+
annotations: {
|
|
129
|
+
readOnlyHint: true,
|
|
130
|
+
destructiveHint: false,
|
|
131
|
+
openWorldHint: false,
|
|
132
|
+
},
|
|
133
|
+
_meta: { securitySchemes: [{ type: "oauth2", scopes: ["mcp"] }] },
|
|
134
|
+
inputSchema: {
|
|
135
|
+
content: contentSchema.describe("Terraform HCL/JSON, CloudFormation YAML/JSON, or Kubernetes YAML/JSON"),
|
|
136
|
+
format: z
|
|
137
|
+
.enum(["terraform", "cloudformation", "kubernetes", "auto"])
|
|
138
|
+
.default("auto"),
|
|
139
|
+
...filters,
|
|
140
|
+
},
|
|
141
|
+
}, async ({ content, format, focus, minSeverity, maxFindings }) => {
|
|
142
|
+
if (!remote) {
|
|
143
|
+
const auth = await requireApiAuth();
|
|
144
|
+
if (!auth.ok)
|
|
145
|
+
return unauthorizedContent(auth);
|
|
146
|
+
}
|
|
147
|
+
try {
|
|
148
|
+
return result(scanIac(content, { format, focus, minSeverity, maxFindings }));
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
return inputError(error);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
return server;
|
|
155
|
+
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skaleagents/swarm",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
8
8
|
"type": "module",
|
|
9
|
-
"description": "SkaleAgents
|
|
9
|
+
"description": "SkaleAgents review tools with OAuth for local and hosted MCP clients",
|
|
10
|
+
"exports": {
|
|
11
|
+
"./http": {
|
|
12
|
+
"types": "./dist/http.d.ts",
|
|
13
|
+
"import": "./dist/http.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
10
16
|
"bin": {
|
|
11
17
|
"skaleagents-swarm": "dist/index.js"
|
|
12
18
|
},
|
|
@@ -18,7 +24,7 @@
|
|
|
18
24
|
"start": "node dist/index.js",
|
|
19
25
|
"dev": "tsx src/index.ts",
|
|
20
26
|
"typecheck": "tsc --noEmit",
|
|
21
|
-
"test": "tsx --test src
|
|
27
|
+
"test": "tsx --test src/*.test.ts src/iac/*.test.ts",
|
|
22
28
|
"smoke": "tsx scripts/smoke-tools.mjs"
|
|
23
29
|
},
|
|
24
30
|
"engines": {
|
|
@@ -26,6 +32,8 @@
|
|
|
26
32
|
},
|
|
27
33
|
"dependencies": {
|
|
28
34
|
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
35
|
+
"hcl2-parser": "^1.0.3",
|
|
36
|
+
"yaml": "^2.9.1",
|
|
29
37
|
"zod": "^4.4.3"
|
|
30
38
|
},
|
|
31
39
|
"devDependencies": {
|