@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.
@@ -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
@@ -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
- : ["Debug mode enabled", "Unpinned container image"].includes(finding.title)
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, 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.",
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 Pulumi YAML"),
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(["terraform", "cloudformation", "kubernetes", "pulumi", "auto"])
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.3";
1
+ export declare const VERSION = "0.6.5";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = "0.6.3";
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.3",
3
+ "version": "0.6.5",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"