@skaleagents/swarm 0.6.5 → 0.6.7

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/README.md CHANGED
@@ -109,7 +109,8 @@ See [client setup](https://skaleagents.com/settings) for Cursor, Claude Code,
109
109
  Claude Desktop, ChatGPT, and Codex instructions.
110
110
 
111
111
  The web app hosts this endpoint using `handleMcpRequest` from
112
- `@skaleagents/swarm/http`. It validates each bearer credential with the API's
112
+ `@skaleagents/swarm/http`. Audit workers import `scanIac` from
113
+ `@skaleagents/swarm/scan`. It validates each bearer credential with the API's
113
114
  `/api/oauth/mcp-token` endpoint before running a tool. Tokens are bound to the
114
115
  MCP resource and cannot access unrelated API routes. Credentials are not
115
116
  forwarded to the public bot directory.
package/dist/iac/parse.js CHANGED
@@ -278,12 +278,31 @@ export function parseIac(content, requested = "auto") {
278
278
  warnings.push("Intrinsic functions and expressions are not evaluated. Only literal configuration is checked.");
279
279
  return { format, resources, warnings };
280
280
  }
281
- function parsePulumiProgram(content) {
282
- const searchable = content
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
283
291
  .replace(/\/\*[\s\S]*?\*\//g, (match) => match.replace(/[^\n]/g, " "))
284
292
  .replace(/(^|[^:])\/\/[^\n]*/g, (match) => match.replace(/[^\n]/g, " "))
285
293
  .replace(/(^|\n)\s*#[^\n]*/g, (match) => match.replace(/[^\n]/g, " "));
294
+ }
295
+ function parsePulumiProgram(content) {
296
+ const searchable = stripProgramComments(content);
286
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
+ };
287
306
  for (const match of searchable.matchAll(/(?:0\.0\.0\.0\/0|::\/0)/g)) {
288
307
  const offset = match.index ?? 0;
289
308
  const window = searchable.slice(Math.max(0, offset - 1500), offset);
@@ -291,26 +310,45 @@ function parsePulumiProgram(content) {
291
310
  const egressAt = window.toLowerCase().lastIndexOf("egress");
292
311
  if (ingressAt < 0 || egressAt > ingressAt)
293
312
  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
- });
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;
305
344
  }
345
+ addProgram("pulumi.program.source", "pulumi:program/source:Source", { program: searchable }, 0, "program");
306
346
  return {
307
347
  format: "pulumi",
308
348
  resources,
309
349
  warnings: [
310
350
  "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."]),
351
+ "Database, storage, IAM, credential, and network checks use literal settings in the submitted program.",
314
352
  ],
315
353
  };
316
354
  }
package/dist/iac/rules.js CHANGED
@@ -242,15 +242,43 @@ function walk(value, visit, path = []) {
242
242
  walk(child, visit, [...path, key]);
243
243
  }
244
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
+ }
245
260
  function checkCompose(resource, check) {
246
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;
247
265
  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"]);
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"]);
254
282
  }
255
283
  function checkCloud(resource, resources, format, check) {
256
284
  const tf = format === "terraform";
package/dist/index.js CHANGED
File without changes
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const VERSION = "0.6.5";
1
+ export declare const VERSION = "0.6.7";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = "0.6.5";
1
+ export const VERSION = "0.6.7";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skaleagents/swarm",
3
- "version": "0.6.5",
3
+ "version": "0.6.7",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -11,6 +11,10 @@
11
11
  "./http": {
12
12
  "types": "./dist/http.d.ts",
13
13
  "import": "./dist/http.js"
14
+ },
15
+ "./scan": {
16
+ "types": "./dist/iac/scan.d.ts",
17
+ "import": "./dist/iac/scan.js"
14
18
  }
15
19
  },
16
20
  "bin": {