@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.
@@ -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
- /(?:^|\n)\s*(?:import\s+\*\s+as\s+pulumi\b|import\s+pulumi\b|from\s+["']@pulumi\/)/.test(content)) {
52
- throw new ScanInputError("Pulumi TypeScript, Python, and Go programs are not parsed yet. Submit Pulumi YAML.");
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. Pulumi TypeScript, Python, and Go programs are not parsed yet.");
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 === "pulumi") {
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 Pulumi YAML.");
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, or Pulumi YAML. Check security, reliability and cost rules with resource locations and remediation. Pulumi TypeScript, Python, and Go programs are not parsed yet.",
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 Pulumi YAML"),
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(["terraform", "cloudformation", "kubernetes", "pulumi", "auto"])
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.4";
1
+ export declare const VERSION = "0.6.5";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = "0.6.4";
1
+ export const VERSION = "0.6.5";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skaleagents/swarm",
3
- "version": "0.6.4",
3
+ "version": "0.6.5",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"