@skaleagents/swarm 0.3.1 → 0.5.0

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.
@@ -0,0 +1,450 @@
1
+ import { array, object } from "./parse.js";
2
+ import { externalHttp, literalCredential } from "../review.js";
3
+ export const rules = {
4
+ SEC001: {
5
+ category: "security",
6
+ severity: "high",
7
+ title: "Hardcoded credential-like value",
8
+ detail: "A credential property contains a literal value.",
9
+ remediation: "Move the value to a secret manager or runtime secret reference. Rotate any credential that has been exposed.",
10
+ },
11
+ SEC002: {
12
+ category: "security",
13
+ severity: "critical",
14
+ title: "Possible AWS access key",
15
+ detail: "A value matches an AWS access-key identifier pattern.",
16
+ remediation: "Remove the key from source, rotate the credential, and check repository history.",
17
+ },
18
+ SEC003: {
19
+ category: "security",
20
+ severity: "critical",
21
+ title: "Private key material in source",
22
+ detail: "A value contains a private-key block.",
23
+ remediation: "Revoke or rotate the key and load its replacement from a secret store.",
24
+ },
25
+ NET002: {
26
+ category: "security",
27
+ severity: "medium",
28
+ title: "Unencrypted HTTP endpoint",
29
+ detail: "A configured URL uses HTTP outside loopback.",
30
+ remediation: "Use HTTPS for service traffic and keep certificate verification enabled.",
31
+ },
32
+ NET001: {
33
+ category: "security",
34
+ severity: "high",
35
+ title: "Broad network exposure",
36
+ detail: "An ingress rule allows every IPv4 or IPv6 source.",
37
+ remediation: "Restrict ingress to approved CIDRs or source security groups. Keep intentional public web traffic behind an authenticated edge.",
38
+ },
39
+ IAM001: {
40
+ category: "security",
41
+ severity: "high",
42
+ title: "Wildcard permission detected",
43
+ detail: "An Allow statement grants wildcard actions or principals.",
44
+ remediation: "List the required actions and trusted principals explicitly, with resource and condition restrictions.",
45
+ },
46
+ DATA001: {
47
+ category: "security",
48
+ severity: "high",
49
+ title: "Public data access",
50
+ detail: "The storage configuration permits public access.",
51
+ remediation: "Remove public ACLs and policies. Enable all public-access blocks unless this is an intentional public asset bucket.",
52
+ },
53
+ DATA002: {
54
+ category: "security",
55
+ severity: "high",
56
+ title: "Storage encryption disabled",
57
+ detail: "Encryption at rest is explicitly disabled.",
58
+ remediation: "Enable encryption with a managed or customer-managed key and plan migration of existing unencrypted data.",
59
+ },
60
+ DATA003: {
61
+ category: "reliability",
62
+ severity: "medium",
63
+ title: "Object versioning not enabled",
64
+ detail: "This bucket has no enabled versioning configuration in the submitted input.",
65
+ remediation: "Enable bucket versioning to recover overwritten or deleted objects. Configure lifecycle retention for older versions.",
66
+ },
67
+ DB001: {
68
+ category: "security",
69
+ severity: "high",
70
+ title: "Database publicly accessible",
71
+ detail: "The database enables public network access.",
72
+ remediation: "Disable public accessibility and connect through private subnets and restricted security groups.",
73
+ },
74
+ DB002: {
75
+ category: "reliability",
76
+ severity: "high",
77
+ title: "Database backups disabled",
78
+ detail: "Automated database backup retention is set to zero.",
79
+ remediation: "Set a nonzero retention period and test restoration against recovery objectives.",
80
+ },
81
+ DB003: {
82
+ category: "reliability",
83
+ severity: "medium",
84
+ title: "Database deletion protection disabled",
85
+ detail: "Deletion protection is explicitly disabled.",
86
+ remediation: "Enable deletion protection for persistent databases and require a reviewed decommissioning process.",
87
+ },
88
+ DB004: {
89
+ category: "reliability",
90
+ severity: "medium",
91
+ title: "Database lacks multi-zone failover",
92
+ detail: "Multi-zone availability is explicitly disabled.",
93
+ remediation: "Enable multi-zone failover for workloads that require continued service during a zone failure.",
94
+ },
95
+ VM001: {
96
+ category: "security",
97
+ severity: "high",
98
+ title: "Instance metadata tokens not required",
99
+ detail: "This EC2 instance does not explicitly require IMDSv2 tokens.",
100
+ remediation: "Set metadata_options.http_tokens or MetadataOptions.HttpTokens to required. Verify the account-level metadata defaults too.",
101
+ },
102
+ COST001: {
103
+ category: "cost",
104
+ severity: "info",
105
+ title: "Compute sizing needs review",
106
+ detail: "The resource declares a compute size.",
107
+ remediation: "Compare CPU and memory utilization with the chosen size before changing capacity or purchasing commitments.",
108
+ },
109
+ K8S001: {
110
+ category: "security",
111
+ severity: "critical",
112
+ title: "Elevated container privileges",
113
+ detail: "A container enables privileged mode or privilege escalation.",
114
+ remediation: "Set privileged and allowPrivilegeEscalation to false. Isolate workloads that genuinely require elevated privileges.",
115
+ },
116
+ K8S002: {
117
+ category: "security",
118
+ severity: "high",
119
+ title: "Host namespace access",
120
+ detail: "The workload shares a host network, process, or IPC namespace.",
121
+ remediation: "Disable hostNetwork, hostPID, and hostIPC unless required by a reviewed node-level component.",
122
+ },
123
+ K8S003: {
124
+ category: "security",
125
+ severity: "high",
126
+ title: "Host filesystem mounted",
127
+ detail: "A volume mounts a host path into the workload.",
128
+ remediation: "Use a PersistentVolumeClaim or a scoped ephemeral volume instead of hostPath.",
129
+ },
130
+ K8S004: {
131
+ category: "security",
132
+ severity: "medium",
133
+ title: "Non-root execution not enforced",
134
+ detail: "Neither the container nor pod enforces runAsNonRoot, or the container selects UID 0.",
135
+ remediation: "Set runAsNonRoot to true and use a nonzero UID supported by the image.",
136
+ },
137
+ K8S005: {
138
+ category: "security",
139
+ severity: "medium",
140
+ title: "Writable container root filesystem",
141
+ detail: "The container does not enforce a read-only root filesystem.",
142
+ remediation: "Set readOnlyRootFilesystem to true and mount writable volumes only where needed.",
143
+ },
144
+ K8S006: {
145
+ category: "security",
146
+ severity: "high",
147
+ title: "Dangerous Linux capabilities",
148
+ detail: "The container adds ALL, SYS_ADMIN, NET_ADMIN, or SYS_PTRACE.",
149
+ remediation: "Drop ALL capabilities and add back only those required by the process.",
150
+ },
151
+ K8S007: {
152
+ category: "reliability",
153
+ severity: "medium",
154
+ title: "Unpinned container image",
155
+ detail: "The image has no version tag or uses latest.",
156
+ remediation: "Pin an immutable digest or a release tag with an immutability policy.",
157
+ },
158
+ K8S008: {
159
+ category: "reliability",
160
+ severity: "medium",
161
+ title: "Container resource bounds missing",
162
+ detail: "CPU or memory requests, or the memory limit, are missing.",
163
+ remediation: "Set measured CPU and memory requests and a memory limit. Review throttling before adding CPU limits.",
164
+ },
165
+ K8S009: {
166
+ category: "reliability",
167
+ severity: "medium",
168
+ title: "Readiness probe missing",
169
+ detail: "A serving container has no readiness probe.",
170
+ remediation: "Add a readiness probe that confirms the process can accept traffic.",
171
+ },
172
+ K8S010: {
173
+ category: "reliability",
174
+ severity: "medium",
175
+ title: "Liveness probe missing",
176
+ detail: "A long-running container has no liveness probe.",
177
+ remediation: "Add a liveness probe and, for slow startup, a startup probe. Avoid restarting on dependency outages.",
178
+ },
179
+ K8S011: {
180
+ category: "reliability",
181
+ severity: "medium",
182
+ title: "Single workload replica",
183
+ detail: "The workload requests fewer than two replicas and no matching autoscaler was submitted.",
184
+ remediation: "Use at least two replicas for availability-sensitive services and spread them across nodes or zones.",
185
+ },
186
+ K8S012: {
187
+ category: "security",
188
+ severity: "high",
189
+ title: "Secret stored in manifest",
190
+ detail: "A Secret manifest contains inline data. Base64 encoding does not protect credentials.",
191
+ remediation: "Load secret values from an external secret store and keep plaintext and base64-encoded credentials out of source control.",
192
+ },
193
+ K8S013: {
194
+ category: "security",
195
+ severity: "high",
196
+ title: "Wildcard Kubernetes RBAC",
197
+ detail: "A role grants wildcard verbs, resources, or API groups.",
198
+ remediation: "Scope the role to the required API groups, resource names, and verbs.",
199
+ },
200
+ };
201
+ export function resourceFindings(resources, format) {
202
+ const findings = [];
203
+ const checked = new Set();
204
+ for (const resource of resources) {
205
+ const check = (id, condition, path = []) => {
206
+ checked.add(id);
207
+ if (condition)
208
+ findings.push({
209
+ ruleId: id,
210
+ ...rules[id],
211
+ resource: resource.id,
212
+ location: resource.locate(path),
213
+ });
214
+ };
215
+ walk(resource.value, (key, value, path, parent) => {
216
+ if (typeof value !== "string")
217
+ return;
218
+ const credentialName = /(?:password|passwd|secret|api[_-]?key|auth[_-]?token)$/i;
219
+ check("SEC001", (credentialName.test(key) ||
220
+ (key === "value" && credentialName.test(String(parent.name)))) &&
221
+ literalCredential(value), path);
222
+ check("SEC002", /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/.test(value), path);
223
+ check("SEC003", /-----BEGIN (?:RSA |EC |OPENSSH |DSA |)PRIVATE KEY-----/.test(value), path);
224
+ check("NET002", [...value.matchAll(/\bhttp:\/\/[^\s"'<>]+/gi)].some((m) => externalHttp(m[0])), path);
225
+ });
226
+ if (format === "kubernetes")
227
+ checkKubernetes(resource, resources, check);
228
+ else
229
+ checkCloud(resource, resources, format, check);
230
+ }
231
+ return { findings, checked };
232
+ }
233
+ function walk(value, visit, path = []) {
234
+ if (Array.isArray(value))
235
+ value.forEach((child, index) => walk(child, visit, [...path, index]));
236
+ else
237
+ for (const [key, child] of Object.entries(object(value))) {
238
+ visit(key, child, [...path, key], object(value));
239
+ if (child && typeof child === "object")
240
+ walk(child, visit, [...path, key]);
241
+ }
242
+ }
243
+ function checkCloud(resource, resources, format, check) {
244
+ const tf = format === "terraform";
245
+ const value = tf ? resource.value : object(resource.value.Properties);
246
+ const prefix = tf ? [] : ["Properties"];
247
+ const property = (hcl, cfn) => value[tf ? hcl : cfn];
248
+ const path = (hcl, cfn) => [...prefix, tf ? hcl : cfn];
249
+ walk(value, (key, child, at, parent) => {
250
+ const isIngress = !at.some((part) => /^(?:egress|SecurityGroupEgress)$/i.test(String(part))) &&
251
+ !/egress/i.test(resource.type) &&
252
+ value.type !== "egress";
253
+ if (/^(?:cidr_blocks|ipv6_cidr_blocks|cidr_ipv4|cidr_ipv6|CidrIp|CidrIpv6)$/.test(key) &&
254
+ isIngress) {
255
+ check("NET001", array(child).some((cidr) => cidr === "0.0.0.0/0" || cidr === "::/0"), [...prefix, ...at]);
256
+ }
257
+ if (/^(?:Action|Principal|actions|principals)$/.test(key) &&
258
+ (parent.Effect === "Allow" || parent.effect === "Allow")) {
259
+ const values = typeof child === "object" && !Array.isArray(child)
260
+ ? Object.values(object(child)).flatMap(array)
261
+ : array(child);
262
+ check("IAM001", values.some((item) => typeof item === "string" && item.includes("*")), [...prefix, ...at]);
263
+ }
264
+ if (/^(?:policy|assume_role_policy|PolicyDocument)$/.test(key) &&
265
+ typeof child === "string" &&
266
+ child.trimStart().startsWith("{")) {
267
+ try {
268
+ const document = JSON.parse(child);
269
+ for (const statement of array(object(document).Statement)) {
270
+ const s = object(statement);
271
+ check("IAM001", s.Effect === "Allow" &&
272
+ [
273
+ ...array(s.Action),
274
+ ...array(s.Principal),
275
+ ...Object.values(object(s.Principal)).flatMap(array),
276
+ ].some((v) => typeof v === "string" && v.includes("*")), [...prefix, ...at]);
277
+ }
278
+ }
279
+ catch {
280
+ /* Nonliteral policies are covered by the expression warning. */
281
+ }
282
+ }
283
+ if (/^(?:instance_type|machine_type|vm_size|InstanceType|DBInstanceClass|instance_class)$/.test(key))
284
+ check("COST001", typeof child === "string", [...prefix, ...at]);
285
+ });
286
+ if (["aws_s3_bucket", "aws_s3_bucket_acl", "AWS::S3::Bucket"].includes(resource.type)) {
287
+ check("DATA001", [
288
+ "public-read",
289
+ "public-read-write",
290
+ "PublicRead",
291
+ "PublicReadWrite",
292
+ "AuthenticatedRead",
293
+ "authenticated-read",
294
+ ].includes(String(property("acl", "AccessControl"))), path("acl", "AccessControl"));
295
+ }
296
+ if (["aws_s3_bucket", "AWS::S3::Bucket"].includes(resource.type)) {
297
+ const attached = resources.find((r) => r.type === "aws_s3_bucket_versioning" &&
298
+ r.value.bucket === `\${${resource.id}.id}`);
299
+ const config = tf
300
+ ? object(array(value.versioning)[0])
301
+ : object(value.VersioningConfiguration);
302
+ const separate = object(array(attached?.value.versioning_configuration)[0]);
303
+ const enabled = tf
304
+ ? config.enabled === true || separate.status === "Enabled"
305
+ : config.Status === "Enabled";
306
+ const dynamic = [config.enabled, config.Status, separate.status].some((v) => typeof v === "object" || (typeof v === "string" && v.includes("${")));
307
+ check("DATA003", !enabled && !dynamic, path("versioning", "VersioningConfiguration"));
308
+ }
309
+ if (["aws_s3_bucket_public_access_block", "AWS::S3::Bucket"].includes(resource.type)) {
310
+ const config = tf ? value : object(value.PublicAccessBlockConfiguration);
311
+ for (const key of tf
312
+ ? [
313
+ "block_public_acls",
314
+ "block_public_policy",
315
+ "ignore_public_acls",
316
+ "restrict_public_buckets",
317
+ ]
318
+ : [
319
+ "BlockPublicAcls",
320
+ "BlockPublicPolicy",
321
+ "IgnorePublicAcls",
322
+ "RestrictPublicBuckets",
323
+ ]) {
324
+ check("DATA001", config[key] === false, tf ? [key] : ["Properties", "PublicAccessBlockConfiguration", key]);
325
+ }
326
+ }
327
+ if ([
328
+ "aws_db_instance",
329
+ "aws_rds_cluster",
330
+ "AWS::RDS::DBInstance",
331
+ "AWS::RDS::DBCluster",
332
+ ].includes(resource.type)) {
333
+ check("DB001", property("publicly_accessible", "PubliclyAccessible") === true, path("publicly_accessible", "PubliclyAccessible"));
334
+ check("DATA002", property("storage_encrypted", "StorageEncrypted") === false, path("storage_encrypted", "StorageEncrypted"));
335
+ check("DB002", property("backup_retention_period", "BackupRetentionPeriod") === 0, path("backup_retention_period", "BackupRetentionPeriod"));
336
+ check("DB003", property("deletion_protection", "DeletionProtection") === false, path("deletion_protection", "DeletionProtection"));
337
+ check("DB004", property("multi_az", "MultiAZ") === false, path("multi_az", "MultiAZ"));
338
+ }
339
+ if (["aws_ebs_volume", "AWS::EC2::Volume"].includes(resource.type))
340
+ check("DATA002", property("encrypted", "Encrypted") === false, path("encrypted", "Encrypted"));
341
+ if (["aws_instance", "AWS::EC2::Instance"].includes(resource.type)) {
342
+ const metadata = tf
343
+ ? object(array(value.metadata_options)[0])
344
+ : object(value.MetadataOptions);
345
+ const tokens = metadata[tf ? "http_tokens" : "HttpTokens"];
346
+ const endpoint = metadata[tf ? "http_endpoint" : "HttpEndpoint"];
347
+ check("VM001", endpoint !== "disabled" &&
348
+ (tokens === undefined || tokens === "optional"), path("metadata_options", "MetadataOptions"));
349
+ }
350
+ }
351
+ function checkKubernetes(resource, resources, check) {
352
+ const value = resource.value;
353
+ const spec = object(value.spec);
354
+ if (resource.type === "Secret")
355
+ check("K8S012", Object.keys(object(value.data)).length +
356
+ Object.keys(object(value.stringData)).length >
357
+ 0);
358
+ if (["Role", "ClusterRole"].includes(resource.type)) {
359
+ array(value.rules).forEach((rule, index) => {
360
+ const r = object(rule);
361
+ check("K8S013", [
362
+ ...array(r.verbs),
363
+ ...array(r.resources),
364
+ ...array(r.apiGroups),
365
+ ].includes("*"), ["rules", index]);
366
+ });
367
+ }
368
+ const workload = [
369
+ "Pod",
370
+ "Deployment",
371
+ "StatefulSet",
372
+ "DaemonSet",
373
+ "ReplicaSet",
374
+ "ReplicationController",
375
+ "Job",
376
+ "CronJob",
377
+ ].includes(resource.type);
378
+ if (!workload)
379
+ return;
380
+ const prefix = resource.type === "Pod"
381
+ ? ["spec"]
382
+ : resource.type === "CronJob"
383
+ ? ["spec", "jobTemplate", "spec", "template", "spec"]
384
+ : ["spec", "template", "spec"];
385
+ let pod = value;
386
+ for (const key of prefix)
387
+ pod = object(pod)[key];
388
+ const podSpec = object(pod);
389
+ const podSecurity = object(podSpec.securityContext);
390
+ for (const key of ["hostNetwork", "hostPID", "hostIPC"])
391
+ check("K8S002", podSpec[key] === true, [...prefix, key]);
392
+ array(podSpec.volumes).forEach((volume, index) => check("K8S003", object(volume).hostPath != null, [
393
+ ...prefix,
394
+ "volumes",
395
+ index,
396
+ "hostPath",
397
+ ]));
398
+ if ([
399
+ "Deployment",
400
+ "StatefulSet",
401
+ "ReplicaSet",
402
+ "ReplicationController",
403
+ ].includes(resource.type)) {
404
+ const metadata = object(value.metadata);
405
+ const hasAutoscaler = resources.some((r) => {
406
+ const target = object(object(r.value.spec).scaleTargetRef);
407
+ return (r.type === "HorizontalPodAutoscaler" &&
408
+ target.kind === resource.type &&
409
+ target.name === metadata.name &&
410
+ (object(r.value.metadata).namespace ?? "default") ===
411
+ (metadata.namespace ?? "default"));
412
+ });
413
+ check("K8S011", !hasAutoscaler &&
414
+ (spec.replicas === undefined ||
415
+ (typeof spec.replicas === "number" && spec.replicas < 2)), ["spec", "replicas"]);
416
+ }
417
+ for (const group of ["containers", "initContainers", "ephemeralContainers"]) {
418
+ array(podSpec[group]).forEach((raw, index) => {
419
+ const container = object(raw);
420
+ const at = [...prefix, group, index];
421
+ const security = object(container.securityContext);
422
+ check("K8S001", security.privileged === true ||
423
+ security.allowPrivilegeEscalation === true, [...at, "securityContext"]);
424
+ check("K8S004", (security.runAsNonRoot ?? podSecurity.runAsNonRoot) !== true ||
425
+ (security.runAsUser ?? podSecurity.runAsUser) === 0, [...at, "securityContext"]);
426
+ check("K8S005", security.readOnlyRootFilesystem !== true, [
427
+ ...at,
428
+ "securityContext",
429
+ ]);
430
+ check("K8S006", array(object(security.capabilities).add).some((v) => ["ALL", "SYS_ADMIN", "NET_ADMIN", "SYS_PTRACE"].includes(String(v))), [...at, "securityContext", "capabilities"]);
431
+ const image = typeof container.image === "string" ? container.image : "";
432
+ check("K8S007", image &&
433
+ !image.includes("@sha256:") &&
434
+ (!image.split("/").at(-1)?.includes(":") ||
435
+ image.endsWith(":latest")), [...at, "image"]);
436
+ if (group !== "ephemeralContainers") {
437
+ const bounds = object(container.resources);
438
+ const requests = object(bounds.requests);
439
+ check("K8S008", requests.cpu == null ||
440
+ requests.memory == null ||
441
+ object(bounds.limits).memory == null, [...at, "resources"]);
442
+ }
443
+ if (group === "containers" &&
444
+ !["Job", "CronJob"].includes(resource.type)) {
445
+ check("K8S009", array(container.ports).length > 0 && !container.readinessProbe, [...at, "readinessProbe"]);
446
+ check("K8S010", !container.livenessProbe, [...at, "livenessProbe"]);
447
+ }
448
+ });
449
+ }
450
+ }
@@ -0,0 +1,43 @@
1
+ import { type IacFormat } from "./parse.js";
2
+ import type { Finding, FindingSeverity } from "../review.js";
3
+ export declare const severityRank: Record<FindingSeverity, number>;
4
+ export type ScanOptions = {
5
+ format?: IacFormat | "auto";
6
+ focus?: "security" | "reliability" | "cost" | "general";
7
+ minSeverity?: FindingSeverity;
8
+ maxFindings?: number;
9
+ };
10
+ export declare function summarize(findings: Finding[]): {
11
+ critical: number;
12
+ high: number;
13
+ medium: number;
14
+ low: number;
15
+ info: number;
16
+ };
17
+ export declare function filterFindings(findings: Finding[], options: ScanOptions): Finding[];
18
+ export declare function scanIac(content: string, options?: ScanOptions): {
19
+ status: string;
20
+ engineVersion: string;
21
+ format: IacFormat;
22
+ focus: "security" | "reliability" | "cost" | "general";
23
+ summary: string;
24
+ parsedResourceCount: number;
25
+ resources: {
26
+ id: string;
27
+ type: string;
28
+ location: import("./parse.js").Location;
29
+ }[];
30
+ findings: Finding[];
31
+ totals: {
32
+ critical: number;
33
+ high: number;
34
+ medium: number;
35
+ low: number;
36
+ info: number;
37
+ };
38
+ totalFindings: number;
39
+ truncated: boolean;
40
+ rulesEvaluated: string[];
41
+ warnings: string[];
42
+ limitations: string[];
43
+ };
@@ -0,0 +1,59 @@
1
+ import { parseIac } from "./parse.js";
2
+ import { resourceFindings } from "./rules.js";
3
+ export const severityRank = {
4
+ info: 0,
5
+ low: 1,
6
+ medium: 2,
7
+ high: 3,
8
+ critical: 4,
9
+ };
10
+ export function summarize(findings) {
11
+ const bySeverity = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
12
+ for (const finding of findings)
13
+ bySeverity[finding.severity]++;
14
+ return bySeverity;
15
+ }
16
+ export function filterFindings(findings, options) {
17
+ return findings
18
+ .filter((f) => (!options.focus ||
19
+ options.focus === "general" ||
20
+ f.category === options.focus) &&
21
+ severityRank[f.severity] >= severityRank[options.minSeverity ?? "info"])
22
+ .sort((a, b) => severityRank[b.severity] - severityRank[a.severity] ||
23
+ (a.location?.line ?? 0) - (b.location?.line ?? 0) ||
24
+ (a.ruleId ?? "").localeCompare(b.ruleId ?? ""));
25
+ }
26
+ export function scanIac(content, options = {}) {
27
+ const parsed = parseIac(content, options.format);
28
+ const result = resourceFindings(parsed.resources, parsed.format);
29
+ const findings = filterFindings(result.findings, options);
30
+ const limit = options.maxFindings ?? 100;
31
+ return {
32
+ status: "completed",
33
+ engineVersion: "0.5.0",
34
+ format: parsed.format,
35
+ focus: options.focus ?? "general",
36
+ summary: `Scanned ${parsed.resources.length} resources; ${findings.length} findings match the selected filters.`,
37
+ parsedResourceCount: parsed.resources.length,
38
+ resources: parsed.resources.map((r) => ({
39
+ id: r.id,
40
+ type: r.type,
41
+ location: r.locate(),
42
+ })),
43
+ findings: findings.slice(0, limit),
44
+ totals: summarize(findings),
45
+ totalFindings: findings.length,
46
+ truncated: findings.length > limit,
47
+ rulesEvaluated: [...result.checked].sort(),
48
+ warnings: parsed.warnings,
49
+ limitations: [
50
+ "Static configuration review only. No cloud credentials, deployments, external modules, or runtime state are inspected.",
51
+ "Resource-specific checks cover Kubernetes workloads, RBAC and Secrets, plus AWS networking, IAM, S3, RDS, EC2 and EBS. Other resources receive literal credential and URL checks only.",
52
+ ...(parsed.format === "terraform"
53
+ ? [
54
+ "HCL locations point to resource declarations; property paths identify the affected setting.",
55
+ ]
56
+ : []),
57
+ ],
58
+ };
59
+ }
package/dist/index.js CHANGED
@@ -1,91 +1,9 @@
1
1
  #!/usr/bin/env node
2
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4
- import { z } from "zod";
5
- import { getApiAccessToken, requireApiAuth, unauthorizedContent, } from "./auth.js";
6
- import { architectureFindings, countIacResources, fetchPublicBotHints, } from "./review.js";
7
- const server = new McpServer({
8
- name: "skaleagents-swarm",
9
- version: "0.3.1",
10
- });
11
- server.registerTool("review_architecture", {
12
- title: "Review architecture",
13
- description: "Review application source or infrastructure text for security, reliability, and cost risks.",
14
- inputSchema: {
15
- content: z
16
- .string()
17
- .min(1)
18
- .max(500_000)
19
- .describe("Application source or IaC text to review"),
20
- focus: z
21
- .enum(["security", "reliability", "cost", "general"])
22
- .optional()
23
- .default("general")
24
- .describe("Review focus: security, reliability, cost, general"),
25
- format: z
26
- .enum(["terraform", "cloudformation", "kubernetes", "application", "auto"])
27
- .optional()
28
- .default("auto")
29
- .describe("Content format: terraform, cloudformation, kubernetes, application, auto"),
30
- },
31
- }, async ({ content, focus, format }) => {
32
- const auth = await requireApiAuth();
33
- if (!auth.ok)
34
- return unauthorizedContent(auth);
35
- const token = await getApiAccessToken();
36
- if (!token)
37
- return unauthorizedContent({ ok: false, reason: "oauth_failed" });
38
- const botHints = await fetchPublicBotHints(token);
39
- const focusValue = focus ?? "general";
40
- const formatValue = format ?? "auto";
41
- const output = {
42
- summary: `Structured architecture review from @skaleagents/swarm (focus=${focusValue}, format=${formatValue})`,
43
- findings: architectureFindings(content, focusValue),
44
- botHints,
45
- };
46
- return {
47
- content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
48
- };
49
- });
50
- server.registerTool("scan_iac_stub", {
51
- title: "Scan IaC (stub)",
52
- description: "Stub seam for Phase 2 DevSecOps IaC scanning.",
53
- inputSchema: {
54
- content: z
55
- .string()
56
- .min(1)
57
- .max(500_000)
58
- .describe("Terraform / CloudFormation / Kubernetes YAML"),
59
- format: z
60
- .string()
61
- .optional()
62
- .describe("terraform, cloudformation, kubernetes, auto"),
63
- },
64
- }, async ({ content, format }) => {
65
- const auth = await requireApiAuth();
66
- if (!auth.ok)
67
- return unauthorizedContent(auth);
68
- const formatValue = format === "terraform" ||
69
- format === "cloudformation" ||
70
- format === "kubernetes" ||
71
- format === "auto"
72
- ? format
73
- : "auto";
74
- const output = {
75
- status: "stub",
76
- message: "Full IaC scanning lands in Phase 2 agent-swarm",
77
- format: formatValue,
78
- parsedResourceCount: countIacResources(content),
79
- };
80
- return {
81
- content: [{ type: "text", text: JSON.stringify(output, null, 2) }],
82
- };
83
- });
84
- async function main() {
85
- const transport = new StdioServerTransport();
86
- await server.connect(transport);
87
- }
88
- main().catch((err) => {
89
- console.error(err);
3
+ import { createServer } from "./server.js";
4
+ createServer()
5
+ .connect(new StdioServerTransport())
6
+ .catch((error) => {
7
+ console.error(error);
90
8
  process.exit(1);
91
9
  });
package/dist/review.d.ts CHANGED
@@ -3,7 +3,17 @@ export type Finding = {
3
3
  severity: FindingSeverity;
4
4
  title: string;
5
5
  detail: string;
6
+ ruleId?: string;
7
+ category?: "security" | "reliability" | "cost";
8
+ remediation?: string;
9
+ location?: {
10
+ line: number;
11
+ column: number;
12
+ path: string;
13
+ };
14
+ resource?: string;
6
15
  };
7
16
  export declare function architectureFindings(content: string, focus: string): Finding[];
8
- export declare function fetchPublicBotHints(token: string): Promise<string[]>;
9
- export declare function countIacResources(content: string): number;
17
+ export declare function literalCredential(value: string): boolean;
18
+ export declare function externalHttp(value: string): boolean;
19
+ export declare function fetchPublicBotHints(token?: string): Promise<string[]>;