@skaleagents/swarm 0.6.4 → 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 +29 -0
- package/dist/server.js +12 -4
- 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
|
@@ -131,6 +131,30 @@ export function architectureFindings(content, focus) {
|
|
|
131
131
|
detail: "Retries are set to unlimited. Cap retries and add backoff so a failing dependency cannot amplify load.",
|
|
132
132
|
});
|
|
133
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
|
+
}
|
|
134
158
|
return findings.map((finding, index) => {
|
|
135
159
|
if (index === 0)
|
|
136
160
|
return finding;
|
|
@@ -149,6 +173,9 @@ export function architectureFindings(content, focus) {
|
|
|
149
173
|
"Outbound call has no timeout": /(?:^|[^.\w])(?:timeout|read_timeout|connect_timeout|request_timeout|readTimeout|connectTimeout)\s*[:=]\s*(?:0|false|none)\b/i,
|
|
150
174
|
"Connection pool is capped at one": /(?:pool_size|max_connections|connectionLimit|maxPoolSize|maximumPoolSize|max_pool_size)\s*[:=]\s*1\b/,
|
|
151
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,
|
|
152
179
|
};
|
|
153
180
|
const pattern = matchers[finding.title];
|
|
154
181
|
const matches = pattern
|
|
@@ -175,6 +202,8 @@ export function architectureFindings(content, focus) {
|
|
|
175
202
|
"Outbound call has no timeout",
|
|
176
203
|
"Connection pool is capped at one",
|
|
177
204
|
"Retry limit is unbounded",
|
|
205
|
+
"Database connection limit is unlimited",
|
|
206
|
+
"Memory limit is disabled",
|
|
178
207
|
].includes(finding.title)
|
|
179
208
|
? "reliability"
|
|
180
209
|
: "security"),
|
package/dist/server.js
CHANGED
|
@@ -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();
|
|
@@ -125,7 +126,7 @@ export function createServer(remote = false) {
|
|
|
125
126
|
description: (name === "scan_iac_stub"
|
|
126
127
|
? "Compatibility alias for scan_iac; runs the full scanner. "
|
|
127
128
|
: "") +
|
|
128
|
-
"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.",
|
|
129
130
|
annotations: {
|
|
130
131
|
readOnlyHint: true,
|
|
131
132
|
destructiveHint: false,
|
|
@@ -133,9 +134,16 @@ export function createServer(remote = false) {
|
|
|
133
134
|
},
|
|
134
135
|
_meta: { securitySchemes: [{ type: "oauth2", scopes: ["mcp"] }] },
|
|
135
136
|
inputSchema: {
|
|
136
|
-
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"),
|
|
137
138
|
format: z
|
|
138
|
-
.enum([
|
|
139
|
+
.enum([
|
|
140
|
+
"terraform",
|
|
141
|
+
"cloudformation",
|
|
142
|
+
"kubernetes",
|
|
143
|
+
"pulumi",
|
|
144
|
+
"compose",
|
|
145
|
+
"auto",
|
|
146
|
+
])
|
|
139
147
|
.default("auto"),
|
|
140
148
|
...filters,
|
|
141
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";
|