@skaleagents/swarm 0.6.4 → 0.6.6

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,103 @@ 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 programLocation(content, offset, path) {
282
+ const before = content.slice(0, offset);
283
+ return {
284
+ line: before.split("\n").length,
285
+ column: offset - before.lastIndexOf("\n"),
286
+ path,
287
+ };
288
+ }
289
+ function stripProgramComments(content) {
290
+ return content
291
+ .replace(/\/\*[\s\S]*?\*\//g, (match) => match.replace(/[^\n]/g, " "))
292
+ .replace(/(^|[^:])\/\/[^\n]*/g, (match) => match.replace(/[^\n]/g, " "))
293
+ .replace(/(^|\n)\s*#[^\n]*/g, (match) => match.replace(/[^\n]/g, " "));
294
+ }
295
+ function parsePulumiProgram(content) {
296
+ const searchable = stripProgramComments(content);
297
+ const resources = [];
298
+ const addProgram = (id, type, value, offset, path) => {
299
+ resources.push({
300
+ id,
301
+ type,
302
+ value,
303
+ locate: () => programLocation(content, offset, path),
304
+ });
305
+ };
306
+ for (const match of searchable.matchAll(/(?:0\.0\.0\.0\/0|::\/0)/g)) {
307
+ const offset = match.index ?? 0;
308
+ const window = searchable.slice(Math.max(0, offset - 1500), offset);
309
+ const ingressAt = window.toLowerCase().lastIndexOf("ingress");
310
+ const egressAt = window.toLowerCase().lastIndexOf("egress");
311
+ if (ingressAt < 0 || egressAt > ingressAt)
312
+ continue;
313
+ addProgram(`pulumi.program.ingress.${resources.length + 1}`, "pulumi:program/securityGroup:SecurityGroup", { properties: { ingress: [{ cidrBlocks: [match[0]] }] } }, offset, "ingress.cidrBlocks");
314
+ }
315
+ const database = {};
316
+ const publicDb = /publicly_?accessible\s*[:=]\s*true\b/i.exec(searchable);
317
+ const unencrypted = /storage_?encrypted\s*[:=]\s*false\b/i.exec(searchable);
318
+ const noBackup = /backup_?retention_?period\s*[:=]\s*0\b/i.exec(searchable);
319
+ if (publicDb)
320
+ database.publicly_accessible = true;
321
+ if (unencrypted)
322
+ database.storage_encrypted = false;
323
+ if (noBackup)
324
+ database.backup_retention_period = 0;
325
+ const databaseMatch = publicDb ?? unencrypted ?? noBackup;
326
+ if (databaseMatch) {
327
+ addProgram("pulumi.program.database", "aws_db_instance", { properties: database }, databaseMatch.index, "database");
328
+ }
329
+ const publicAcl = /(?:acl|accessControl)\s*[:=]\s*["']public-read(?:-write)?["']/i.exec(searchable);
330
+ if (publicAcl) {
331
+ addProgram("pulumi.program.bucket", "aws_s3_bucket", { properties: { acl: "public-read" } }, publicAcl.index, "acl");
332
+ }
333
+ for (const match of searchable.matchAll(/actions?\s*[:=]\s*(?:\[\s*)?["']\*["']/gi)) {
334
+ const offset = match.index ?? 0;
335
+ const before = searchable.slice(Math.max(0, offset - 300), offset);
336
+ const effects = [
337
+ ...before.matchAll(/(?:effect|Effect)\s*[:=]\s*["']?(Allow|Deny)["']?/gi),
338
+ ];
339
+ const nearest = effects.at(-1)?.[1] ?? "";
340
+ if (!/^allow$/i.test(nearest))
341
+ continue;
342
+ addProgram("pulumi.program.policy", "pulumi:program/iam:Policy", { properties: { effect: "Allow", actions: ["*"] } }, offset, "actions");
343
+ break;
344
+ }
345
+ addProgram("pulumi.program.source", "pulumi:program/source:Source", { program: searchable }, 0, "program");
346
+ return {
347
+ format: "pulumi",
348
+ resources,
349
+ warnings: [
350
+ "Pulumi programs are scanned as text. Computed values, stacks, and external modules are not evaluated.",
351
+ "Database, storage, IAM, credential, and network checks use literal settings in the submitted program.",
352
+ ],
353
+ };
354
+ }
355
+ function composeServiceMap(root) {
356
+ if (root.apiVersion || root.kind || root.Resources || root.resources)
357
+ return undefined;
358
+ const services = object(root.services);
359
+ const entries = Object.entries(services);
360
+ if (entries.length > 0 &&
361
+ entries.every(([, raw]) => {
362
+ const service = object(raw);
363
+ return ("image" in service ||
364
+ "build" in service ||
365
+ "ports" in service ||
366
+ "privileged" in service);
367
+ }))
368
+ return services;
369
+ return undefined;
370
+ }
261
371
  function pulumiResourceMap(root) {
262
372
  const resources = object(root.resources);
263
373
  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,44 @@ function walk(value, visit, path = []) {
240
242
  walk(child, visit, [...path, key]);
241
243
  }
242
244
  }
245
+ function composePortIsPublic(port) {
246
+ if (typeof port === "string") {
247
+ const value = port.trim();
248
+ if (/^(?:127\.0\.0\.1|localhost):/.test(value))
249
+ return false;
250
+ if (value.includes("0.0.0.0"))
251
+ return true;
252
+ return /^\d+:\d+(?:\/[A-Za-z0-9]+)?$/.test(value);
253
+ }
254
+ const mapping = object(port);
255
+ const host = mapping.host_ip ?? mapping.hostIp;
256
+ if (host === "127.0.0.1" || host === "localhost")
257
+ return false;
258
+ return host === "0.0.0.0" || mapping.published != null || mapping.target != null;
259
+ }
260
+ function checkCompose(resource, check) {
261
+ const service = resource.value;
262
+ const image = typeof service.image === "string" ? service.image : "";
263
+ const limits = object(object(object(service.deploy).resources).limits);
264
+ const user = service.user;
265
+ check("K8S001", service.privileged === true, ["privileged"]);
266
+ check("K8S002", service.network_mode === "host", ["network_mode"]);
267
+ check("K8S003", array(service.volumes).some((volume) => {
268
+ const text = typeof volume === "string"
269
+ ? volume
270
+ : String(object(volume).source ?? object(volume).target ?? "");
271
+ return (text.includes("docker.sock") ||
272
+ /(?:^|:)\/:/.test(text) ||
273
+ text.startsWith("/var/run/"));
274
+ }), ["volumes"]);
275
+ check("K8S004", user === "root" || user === "0" || user === 0, ["user"]);
276
+ check("K8S006", array(service.cap_add).some((cap) => ["ALL", "SYS_ADMIN", "NET_ADMIN", "SYS_PTRACE"].includes(String(cap).toUpperCase())), ["cap_add"]);
277
+ check("K8S007", image !== "" && (image.endsWith(":latest") || !/[:@]/.test(image)), ["image"]);
278
+ check("K8S008", service.mem_limit == null &&
279
+ limits.memory == null &&
280
+ service.mem_reservation == null, ["mem_limit"]);
281
+ check("NET001", array(service.ports).some(composePortIsPublic), ["ports"]);
282
+ }
243
283
  function checkCloud(resource, resources, format, check) {
244
284
  const tf = format === "terraform";
245
285
  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.6";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = "0.6.4";
1
+ export const VERSION = "0.6.6";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skaleagents/swarm",
3
- "version": "0.6.4",
3
+ "version": "0.6.6",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"