@skaleagents/swarm 0.6.3 → 0.6.5
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/iac/parse.d.ts +3 -1
- package/dist/iac/parse.js +78 -6
- package/dist/iac/rules.js +12 -0
- package/dist/review.js +63 -1
- package/dist/server.js +14 -5
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/dist/iac/parse.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type IacFormat = "terraform" | "cloudformation" | "kubernetes" | "pulumi";
|
|
1
|
+
export type IacFormat = "terraform" | "cloudformation" | "kubernetes" | "pulumi" | "compose";
|
|
2
2
|
export type Path = (string | number)[];
|
|
3
3
|
export type Location = {
|
|
4
4
|
line: number;
|
|
@@ -22,5 +22,7 @@ export declare class ScanInputError extends Error {
|
|
|
22
22
|
export declare function object(value: unknown): Record<string, unknown>;
|
|
23
23
|
export declare function array(value: unknown): unknown[];
|
|
24
24
|
export declare function looksLikeIac(content: string): boolean;
|
|
25
|
+
export declare function looksLikePulumiProgram(content: string): boolean;
|
|
26
|
+
export declare function looksLikeCompose(content: string): boolean;
|
|
25
27
|
export declare function looksLikePulumiYaml(content: string): boolean;
|
|
26
28
|
export declare function parseIac(content: string, requested?: IacFormat | "auto"): ParsedIac;
|
package/dist/iac/parse.js
CHANGED
|
@@ -38,7 +38,15 @@ export function looksLikeIac(content) {
|
|
|
38
38
|
return (/^\s*(?:resource|module|terraform|variable|provider)\s+["{]/m.test(content) ||
|
|
39
39
|
/^\s*(?:["']?Resources["']?\s*:|apiVersion\s*:)/m.test(content) ||
|
|
40
40
|
/^\s*\{\s*"(?:resource|Resources|apiVersion|AWSTemplateFormatVersion)"\s*:/.test(content) ||
|
|
41
|
-
looksLikePulumiYaml(content)
|
|
41
|
+
looksLikePulumiYaml(content) ||
|
|
42
|
+
looksLikePulumiProgram(content) ||
|
|
43
|
+
looksLikeCompose(content));
|
|
44
|
+
}
|
|
45
|
+
export function looksLikePulumiProgram(content) {
|
|
46
|
+
return /(?:^|\n)\s*(?:import\s+\*\s+as\s+pulumi\b|import\s+pulumi\b|from\s+["']@pulumi\/)/.test(content);
|
|
47
|
+
}
|
|
48
|
+
export function looksLikeCompose(content) {
|
|
49
|
+
return /^\s*services\s*:/m.test(content) && /^\s+image\s*:/m.test(content);
|
|
42
50
|
}
|
|
43
51
|
export function looksLikePulumiYaml(content) {
|
|
44
52
|
return (/^\s*resources\s*:/m.test(content) &&
|
|
@@ -48,8 +56,10 @@ export function parseIac(content, requested = "auto") {
|
|
|
48
56
|
if (!content.trim())
|
|
49
57
|
throw new ScanInputError("Content must not be empty or whitespace.");
|
|
50
58
|
if (requested !== "terraform" &&
|
|
51
|
-
|
|
52
|
-
|
|
59
|
+
requested !== "compose" &&
|
|
60
|
+
looksLikePulumiProgram(content) &&
|
|
61
|
+
!looksLikePulumiYaml(content)) {
|
|
62
|
+
return parsePulumiProgram(content);
|
|
53
63
|
}
|
|
54
64
|
const warnings = [];
|
|
55
65
|
const resources = [];
|
|
@@ -152,12 +162,18 @@ export function parseIac(content, requested = "auto") {
|
|
|
152
162
|
checkShape(data);
|
|
153
163
|
const root = object(data);
|
|
154
164
|
const pulumiResources = pulumiResourceMap(root);
|
|
165
|
+
const services = composeServiceMap(root);
|
|
155
166
|
let detected;
|
|
156
167
|
if (requested === "pulumi" || (requested === "auto" && pulumiResources)) {
|
|
157
168
|
if (!pulumiResources)
|
|
158
|
-
throw new ScanInputError("Pulumi YAML requires a resources mapping. Each resource needs a type token.
|
|
169
|
+
throw new ScanInputError("Pulumi YAML requires a resources mapping. Each resource needs a type token.");
|
|
159
170
|
detected = "pulumi";
|
|
160
171
|
}
|
|
172
|
+
else if (requested === "compose" || (requested === "auto" && services)) {
|
|
173
|
+
if (!services)
|
|
174
|
+
throw new ScanInputError("Docker Compose requires a services mapping with an image, build, or ports field.");
|
|
175
|
+
detected = "compose";
|
|
176
|
+
}
|
|
161
177
|
else {
|
|
162
178
|
detected = root.Resources
|
|
163
179
|
? "cloudformation"
|
|
@@ -188,7 +204,11 @@ export function parseIac(content, requested = "auto") {
|
|
|
188
204
|
},
|
|
189
205
|
});
|
|
190
206
|
};
|
|
191
|
-
if (format === "
|
|
207
|
+
if (format === "compose") {
|
|
208
|
+
for (const [name, raw] of Object.entries(services ?? {}))
|
|
209
|
+
add(name, "compose:service", raw, ["services", name]);
|
|
210
|
+
}
|
|
211
|
+
else if (format === "pulumi") {
|
|
192
212
|
for (const [name, raw] of Object.entries(pulumiResources ?? {})) {
|
|
193
213
|
const value = object(raw);
|
|
194
214
|
add(name, String(value.type), value, ["resources", name]);
|
|
@@ -251,13 +271,65 @@ export function parseIac(content, requested = "auto") {
|
|
|
251
271
|
}
|
|
252
272
|
}
|
|
253
273
|
if (!format)
|
|
254
|
-
throw new ScanInputError("No IaC document found. Choose Terraform, CloudFormation, Kubernetes, or
|
|
274
|
+
throw new ScanInputError("No IaC document found. Choose Terraform, CloudFormation, Kubernetes, Pulumi, or Docker Compose.");
|
|
255
275
|
if (!resources.length)
|
|
256
276
|
warnings.push("No resources found in the submitted document.");
|
|
257
277
|
if (/(?:!(?:Ref|Sub|GetAtt|If)\b|"(?:Ref|Fn::\w+)"\s*:|\$\{)/.test(content))
|
|
258
278
|
warnings.push("Intrinsic functions and expressions are not evaluated. Only literal configuration is checked.");
|
|
259
279
|
return { format, resources, warnings };
|
|
260
280
|
}
|
|
281
|
+
function parsePulumiProgram(content) {
|
|
282
|
+
const searchable = content
|
|
283
|
+
.replace(/\/\*[\s\S]*?\*\//g, (match) => match.replace(/[^\n]/g, " "))
|
|
284
|
+
.replace(/(^|[^:])\/\/[^\n]*/g, (match) => match.replace(/[^\n]/g, " "))
|
|
285
|
+
.replace(/(^|\n)\s*#[^\n]*/g, (match) => match.replace(/[^\n]/g, " "));
|
|
286
|
+
const resources = [];
|
|
287
|
+
for (const match of searchable.matchAll(/(?:0\.0\.0\.0\/0|::\/0)/g)) {
|
|
288
|
+
const offset = match.index ?? 0;
|
|
289
|
+
const window = searchable.slice(Math.max(0, offset - 1500), offset);
|
|
290
|
+
const ingressAt = window.toLowerCase().lastIndexOf("ingress");
|
|
291
|
+
const egressAt = window.toLowerCase().lastIndexOf("egress");
|
|
292
|
+
if (ingressAt < 0 || egressAt > ingressAt)
|
|
293
|
+
continue;
|
|
294
|
+
const before = content.slice(0, offset);
|
|
295
|
+
resources.push({
|
|
296
|
+
id: `pulumi.program.${resources.length + 1}`,
|
|
297
|
+
type: "pulumi:program/securityGroup:SecurityGroup",
|
|
298
|
+
value: { properties: { ingress: [{ cidrBlocks: [match[0]] }] } },
|
|
299
|
+
locate: () => ({
|
|
300
|
+
line: before.split("\n").length,
|
|
301
|
+
column: offset - before.lastIndexOf("\n"),
|
|
302
|
+
path: "ingress.cidrBlocks",
|
|
303
|
+
}),
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
return {
|
|
307
|
+
format: "pulumi",
|
|
308
|
+
resources,
|
|
309
|
+
warnings: [
|
|
310
|
+
"Pulumi programs are scanned as text. Computed values, stacks, and external modules are not evaluated.",
|
|
311
|
+
...(resources.length
|
|
312
|
+
? []
|
|
313
|
+
: ["No literal ingress CIDRs were found in the Pulumi program."]),
|
|
314
|
+
],
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
function composeServiceMap(root) {
|
|
318
|
+
if (root.apiVersion || root.kind || root.Resources || root.resources)
|
|
319
|
+
return undefined;
|
|
320
|
+
const services = object(root.services);
|
|
321
|
+
const entries = Object.entries(services);
|
|
322
|
+
if (entries.length > 0 &&
|
|
323
|
+
entries.every(([, raw]) => {
|
|
324
|
+
const service = object(raw);
|
|
325
|
+
return ("image" in service ||
|
|
326
|
+
"build" in service ||
|
|
327
|
+
"ports" in service ||
|
|
328
|
+
"privileged" in service);
|
|
329
|
+
}))
|
|
330
|
+
return services;
|
|
331
|
+
return undefined;
|
|
332
|
+
}
|
|
261
333
|
function pulumiResourceMap(root) {
|
|
262
334
|
const resources = object(root.resources);
|
|
263
335
|
const entries = Object.entries(resources);
|
package/dist/iac/rules.js
CHANGED
|
@@ -225,6 +225,8 @@ export function resourceFindings(resources, format) {
|
|
|
225
225
|
});
|
|
226
226
|
if (format === "kubernetes")
|
|
227
227
|
checkKubernetes(resource, resources, check);
|
|
228
|
+
else if (format === "compose")
|
|
229
|
+
checkCompose(resource, check);
|
|
228
230
|
else
|
|
229
231
|
checkCloud(resource, resources, format, check);
|
|
230
232
|
}
|
|
@@ -240,6 +242,16 @@ function walk(value, visit, path = []) {
|
|
|
240
242
|
walk(child, visit, [...path, key]);
|
|
241
243
|
}
|
|
242
244
|
}
|
|
245
|
+
function checkCompose(resource, check) {
|
|
246
|
+
const service = resource.value;
|
|
247
|
+
check("K8S001", service.privileged === true, ["privileged"]);
|
|
248
|
+
check("NET001", array(service.ports).some((port) => {
|
|
249
|
+
if (typeof port === "string")
|
|
250
|
+
return port.includes("0.0.0.0");
|
|
251
|
+
const mapping = object(port);
|
|
252
|
+
return mapping.host_ip === "0.0.0.0" || mapping.hostIp === "0.0.0.0";
|
|
253
|
+
}), ["ports"]);
|
|
254
|
+
}
|
|
243
255
|
function checkCloud(resource, resources, format, check) {
|
|
244
256
|
const tf = format === "terraform";
|
|
245
257
|
const pulumi = format === "pulumi";
|
package/dist/review.js
CHANGED
|
@@ -107,6 +107,54 @@ export function architectureFindings(content, focus) {
|
|
|
107
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.",
|
|
108
108
|
});
|
|
109
109
|
}
|
|
110
|
+
if (shouldInclude(focusValue, "reliability") &&
|
|
111
|
+
/(?:^|[^.\w])(?:timeout|read_timeout|connect_timeout|request_timeout|readTimeout|connectTimeout)\s*[:=]\s*(?:0|false|none)\b/i.test(content)) {
|
|
112
|
+
findings.push({
|
|
113
|
+
severity: "medium",
|
|
114
|
+
title: "Outbound call has no timeout",
|
|
115
|
+
detail: "A timeout is set to zero, false, or none. Set a finite timeout so a stalled dependency cannot hold the process open.",
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
if (shouldInclude(focusValue, "reliability") &&
|
|
119
|
+
/(?:pool_size|max_connections|connectionLimit|maxPoolSize|maximumPoolSize|max_pool_size)\s*[:=]\s*1\b/.test(content)) {
|
|
120
|
+
findings.push({
|
|
121
|
+
severity: "high",
|
|
122
|
+
title: "Connection pool is capped at one",
|
|
123
|
+
detail: "The pool allows one connection. Concurrent requests will queue behind that single connection.",
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
if (shouldInclude(focusValue, "reliability") &&
|
|
127
|
+
/(?:max_?retries|retry_?count|retries)\s*[:=]\s*(?:-1|Infinity)\b/i.test(content)) {
|
|
128
|
+
findings.push({
|
|
129
|
+
severity: "medium",
|
|
130
|
+
title: "Retry limit is unbounded",
|
|
131
|
+
detail: "Retries are set to unlimited. Cap retries and add backoff so a failing dependency cannot amplify load.",
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
if (shouldInclude(focusValue, "reliability") &&
|
|
135
|
+
/(?:max_?open_?conns|maxOpenConns|SetMaxOpenConns)\s*(?:[:=]|\()\s*0\b/.test(content)) {
|
|
136
|
+
findings.push({
|
|
137
|
+
severity: "high",
|
|
138
|
+
title: "Database connection limit is unlimited",
|
|
139
|
+
detail: "The database client allows unlimited open connections. Set a finite pool size so a traffic spike cannot exhaust the database.",
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
if (shouldInclude(focusValue, "reliability") &&
|
|
143
|
+
/(?:mem_limit|memory_limit)\s*[:=]\s*["']?0\b/i.test(content)) {
|
|
144
|
+
findings.push({
|
|
145
|
+
severity: "medium",
|
|
146
|
+
title: "Memory limit is disabled",
|
|
147
|
+
detail: "The memory limit is zero. Set a memory limit so one workload cannot consume the host.",
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
if (shouldInclude(focusValue, "security") &&
|
|
151
|
+
/^\s*USER\s+root\s*$/im.test(content)) {
|
|
152
|
+
findings.push({
|
|
153
|
+
severity: "medium",
|
|
154
|
+
title: "Container runs as root",
|
|
155
|
+
detail: "The container user is root. Run the process as an unprivileged user.",
|
|
156
|
+
});
|
|
157
|
+
}
|
|
110
158
|
return findings.map((finding, index) => {
|
|
111
159
|
if (index === 0)
|
|
112
160
|
return finding;
|
|
@@ -122,6 +170,12 @@ export function architectureFindings(content, focus) {
|
|
|
122
170
|
"Public data access pattern": /(?:public-read|publicRead|allUsers)/i,
|
|
123
171
|
"Debug mode enabled": /(?:debug|app_debug)\s*[:=]\s*["']?(?:true|1|yes)/i,
|
|
124
172
|
"Compute sizing needs review": /(?:instance_type|machine_type|vm_size)\s*[:=]/i,
|
|
173
|
+
"Outbound call has no timeout": /(?:^|[^.\w])(?:timeout|read_timeout|connect_timeout|request_timeout|readTimeout|connectTimeout)\s*[:=]\s*(?:0|false|none)\b/i,
|
|
174
|
+
"Connection pool is capped at one": /(?:pool_size|max_connections|connectionLimit|maxPoolSize|maximumPoolSize|max_pool_size)\s*[:=]\s*1\b/,
|
|
175
|
+
"Retry limit is unbounded": /(?:max_?retries|retry_?count|retries)\s*[:=]\s*(?:-1|Infinity)\b/i,
|
|
176
|
+
"Database connection limit is unlimited": /(?:max_?open_?conns|maxOpenConns|SetMaxOpenConns)\s*(?:[:=]|\()\s*0\b/,
|
|
177
|
+
"Memory limit is disabled": /(?:mem_limit|memory_limit)\s*[:=]\s*["']?0\b/i,
|
|
178
|
+
"Container runs as root": /^\s*USER\s+root\s*$/im,
|
|
125
179
|
};
|
|
126
180
|
const pattern = matchers[finding.title];
|
|
127
181
|
const matches = pattern
|
|
@@ -142,7 +196,15 @@ export function architectureFindings(content, focus) {
|
|
|
142
196
|
ruleId: `APP${String(ruleIndex).padStart(3, "0")}`,
|
|
143
197
|
category: (finding.title === "Compute sizing needs review"
|
|
144
198
|
? "cost"
|
|
145
|
-
: [
|
|
199
|
+
: [
|
|
200
|
+
"Debug mode enabled",
|
|
201
|
+
"Unpinned container image",
|
|
202
|
+
"Outbound call has no timeout",
|
|
203
|
+
"Connection pool is capped at one",
|
|
204
|
+
"Retry limit is unbounded",
|
|
205
|
+
"Database connection limit is unlimited",
|
|
206
|
+
"Memory limit is disabled",
|
|
207
|
+
].includes(finding.title)
|
|
146
208
|
? "reliability"
|
|
147
209
|
: "security"),
|
|
148
210
|
remediation: finding.detail,
|
package/dist/server.js
CHANGED
|
@@ -56,7 +56,7 @@ export function createServer(remote = false) {
|
|
|
56
56
|
registerConsultation(server, remote);
|
|
57
57
|
server.registerTool("review_architecture", {
|
|
58
58
|
title: "Review architecture",
|
|
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.",
|
|
59
|
+
description: "Review individual source snippets or parsed infrastructure for security, reliability, and cost risks. Application checks include infrastructure bottlenecks: a disabled timeout, a one-connection pool, and an unbounded retry limit. For a whole application or Next.js architecture consultation, use plan_architecture_review and review_application_architecture instead.",
|
|
60
60
|
annotations: {
|
|
61
61
|
readOnlyHint: true,
|
|
62
62
|
destructiveHint: false,
|
|
@@ -72,12 +72,13 @@ export function createServer(remote = false) {
|
|
|
72
72
|
"cloudformation",
|
|
73
73
|
"kubernetes",
|
|
74
74
|
"pulumi",
|
|
75
|
+
"compose",
|
|
75
76
|
"application",
|
|
76
77
|
"auto",
|
|
77
78
|
])
|
|
78
79
|
.optional()
|
|
79
80
|
.default("auto")
|
|
80
|
-
.describe("Content format: terraform, cloudformation, kubernetes, pulumi, application, auto"),
|
|
81
|
+
.describe("Content format: terraform, cloudformation, kubernetes, pulumi, compose, application, auto"),
|
|
81
82
|
},
|
|
82
83
|
}, async ({ content, focus, format, minSeverity, maxFindings }) => {
|
|
83
84
|
const botHints = fetchPublicBotHints();
|
|
@@ -108,6 +109,7 @@ export function createServer(remote = false) {
|
|
|
108
109
|
truncated: findings.length > maxFindings,
|
|
109
110
|
limitations: [
|
|
110
111
|
"Application checks are text patterns, not a language-aware or runtime analysis. Findings do not establish that code is safe.",
|
|
112
|
+
"Infrastructure bottleneck checks cover a disabled timeout, a one-connection pool, and an unbounded retry limit.",
|
|
111
113
|
],
|
|
112
114
|
botHints: await botHints,
|
|
113
115
|
});
|
|
@@ -124,7 +126,7 @@ export function createServer(remote = false) {
|
|
|
124
126
|
description: (name === "scan_iac_stub"
|
|
125
127
|
? "Compatibility alias for scan_iac; runs the full scanner. "
|
|
126
128
|
: "") +
|
|
127
|
-
"Parse Terraform HCL/JSON, CloudFormation YAML/JSON, Kubernetes manifests,
|
|
129
|
+
"Parse Terraform HCL/JSON, CloudFormation YAML/JSON, Kubernetes manifests, Pulumi YAML, Pulumi programs, or Docker Compose. Check security, reliability and cost rules with resource locations and remediation. Pulumi programs are scanned as text.",
|
|
128
130
|
annotations: {
|
|
129
131
|
readOnlyHint: true,
|
|
130
132
|
destructiveHint: false,
|
|
@@ -132,9 +134,16 @@ export function createServer(remote = false) {
|
|
|
132
134
|
},
|
|
133
135
|
_meta: { securitySchemes: [{ type: "oauth2", scopes: ["mcp"] }] },
|
|
134
136
|
inputSchema: {
|
|
135
|
-
content: contentSchema.describe("Terraform HCL/JSON, CloudFormation YAML/JSON, Kubernetes YAML/JSON, or
|
|
137
|
+
content: contentSchema.describe("Terraform HCL/JSON, CloudFormation YAML/JSON, Kubernetes YAML/JSON, Pulumi YAML or program text, or Docker Compose"),
|
|
136
138
|
format: z
|
|
137
|
-
.enum([
|
|
139
|
+
.enum([
|
|
140
|
+
"terraform",
|
|
141
|
+
"cloudformation",
|
|
142
|
+
"kubernetes",
|
|
143
|
+
"pulumi",
|
|
144
|
+
"compose",
|
|
145
|
+
"auto",
|
|
146
|
+
])
|
|
138
147
|
.default("auto"),
|
|
139
148
|
...filters,
|
|
140
149
|
},
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.6.
|
|
1
|
+
export declare const VERSION = "0.6.5";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.6.
|
|
1
|
+
export const VERSION = "0.6.5";
|