@skaleagents/swarm 0.6.7 → 0.6.9
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/dist/iac/parse.d.ts +3 -1
- package/dist/iac/parse.js +269 -2
- package/dist/iac/rules.js +67 -0
- package/dist/index.js +0 -0
- package/dist/server.js +7 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +5 -1
package/dist/iac/parse.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type IacFormat = "terraform" | "cloudformation" | "kubernetes" | "pulumi" | "compose";
|
|
1
|
+
export type IacFormat = "terraform" | "cloudformation" | "kubernetes" | "pulumi" | "compose" | "dockerfile" | "github";
|
|
2
2
|
export type Path = (string | number)[];
|
|
3
3
|
export type Location = {
|
|
4
4
|
line: number;
|
|
@@ -24,5 +24,7 @@ export declare function array(value: unknown): unknown[];
|
|
|
24
24
|
export declare function looksLikeIac(content: string): boolean;
|
|
25
25
|
export declare function looksLikePulumiProgram(content: string): boolean;
|
|
26
26
|
export declare function looksLikeCompose(content: string): boolean;
|
|
27
|
+
export declare function looksLikeDockerfile(content: string): boolean;
|
|
28
|
+
export declare function looksLikeGithubWorkflow(content: string): boolean;
|
|
27
29
|
export declare function looksLikePulumiYaml(content: string): boolean;
|
|
28
30
|
export declare function parseIac(content: string, requested?: IacFormat | "auto"): ParsedIac;
|
package/dist/iac/parse.js
CHANGED
|
@@ -40,7 +40,9 @@ export function looksLikeIac(content) {
|
|
|
40
40
|
/^\s*\{\s*"(?:resource|Resources|apiVersion|AWSTemplateFormatVersion)"\s*:/.test(content) ||
|
|
41
41
|
looksLikePulumiYaml(content) ||
|
|
42
42
|
looksLikePulumiProgram(content) ||
|
|
43
|
-
looksLikeCompose(content)
|
|
43
|
+
looksLikeCompose(content) ||
|
|
44
|
+
looksLikeDockerfile(content) ||
|
|
45
|
+
looksLikeGithubWorkflow(content));
|
|
44
46
|
}
|
|
45
47
|
export function looksLikePulumiProgram(content) {
|
|
46
48
|
return /(?:^|\n)\s*(?:import\s+\*\s+as\s+pulumi\b|import\s+pulumi\b|from\s+["']@pulumi\/)/.test(content);
|
|
@@ -48,6 +50,23 @@ export function looksLikePulumiProgram(content) {
|
|
|
48
50
|
export function looksLikeCompose(content) {
|
|
49
51
|
return /^\s*services\s*:/m.test(content) && /^\s+image\s*:/m.test(content);
|
|
50
52
|
}
|
|
53
|
+
export function looksLikeDockerfile(content) {
|
|
54
|
+
return /^\s*FROM\s+\S+/m.test(content);
|
|
55
|
+
}
|
|
56
|
+
function topLevelKey(content, name) {
|
|
57
|
+
return new RegExp(`^(?:"${name}"|'${name}'|${name})\\s*:`, "m").test(content);
|
|
58
|
+
}
|
|
59
|
+
export function looksLikeGithubWorkflow(content) {
|
|
60
|
+
if (topLevelKey(content, "apiVersion") || topLevelKey(content, "kind"))
|
|
61
|
+
return false;
|
|
62
|
+
const topOn = topLevelKey(content, "on");
|
|
63
|
+
const composeOnly = topLevelKey(content, "services") &&
|
|
64
|
+
/^\s+image\s*:/m.test(content) &&
|
|
65
|
+
!topOn;
|
|
66
|
+
if (composeOnly)
|
|
67
|
+
return false;
|
|
68
|
+
return topOn && topLevelKey(content, "jobs");
|
|
69
|
+
}
|
|
51
70
|
export function looksLikePulumiYaml(content) {
|
|
52
71
|
return (/^\s*resources\s*:/m.test(content) &&
|
|
53
72
|
/^\s+type\s*:\s*[A-Za-z0-9_.-]+:\S+/m.test(content));
|
|
@@ -55,6 +74,16 @@ export function looksLikePulumiYaml(content) {
|
|
|
55
74
|
export function parseIac(content, requested = "auto") {
|
|
56
75
|
if (!content.trim())
|
|
57
76
|
throw new ScanInputError("Content must not be empty or whitespace.");
|
|
77
|
+
// Detect these before HCL or a failing YAML parse so a Dockerfile is not classified as Terraform.
|
|
78
|
+
if (requested === "dockerfile" ||
|
|
79
|
+
((requested === "auto" || requested === "terraform") &&
|
|
80
|
+
looksLikeDockerfile(content))) {
|
|
81
|
+
return parseDockerfile(content);
|
|
82
|
+
}
|
|
83
|
+
if (requested === "github" ||
|
|
84
|
+
(requested === "auto" && looksLikeGithubWorkflow(content))) {
|
|
85
|
+
return parseGithubWorkflow(content);
|
|
86
|
+
}
|
|
58
87
|
if (requested !== "terraform" &&
|
|
59
88
|
requested !== "compose" &&
|
|
60
89
|
looksLikePulumiProgram(content) &&
|
|
@@ -271,7 +300,7 @@ export function parseIac(content, requested = "auto") {
|
|
|
271
300
|
}
|
|
272
301
|
}
|
|
273
302
|
if (!format)
|
|
274
|
-
throw new ScanInputError("No IaC document found. Choose Terraform, CloudFormation, Kubernetes, Pulumi, or
|
|
303
|
+
throw new ScanInputError("No IaC document found. Choose Terraform, CloudFormation, Kubernetes, Pulumi, Docker Compose, a Dockerfile, or a GitHub Actions workflow.");
|
|
275
304
|
if (!resources.length)
|
|
276
305
|
warnings.push("No resources found in the submitted document.");
|
|
277
306
|
if (/(?:!(?:Ref|Sub|GetAtt|If)\b|"(?:Ref|Fn::\w+)"\s*:|\$\{)/.test(content))
|
|
@@ -352,6 +381,244 @@ function parsePulumiProgram(content) {
|
|
|
352
381
|
],
|
|
353
382
|
};
|
|
354
383
|
}
|
|
384
|
+
const submittedTextWarning = "The scan reads the submitted text and does not execute the image or the workflow.";
|
|
385
|
+
function logicalDockerfileLines(content) {
|
|
386
|
+
const raw = content.split(/\r?\n/);
|
|
387
|
+
const lines = [];
|
|
388
|
+
let buffer = "";
|
|
389
|
+
let start = 1;
|
|
390
|
+
for (let index = 0; index < raw.length; index++) {
|
|
391
|
+
const current = raw[index].replace(/\s+$/, "");
|
|
392
|
+
if (!buffer)
|
|
393
|
+
start = index + 1;
|
|
394
|
+
if (current.endsWith("\\")) {
|
|
395
|
+
buffer += `${current.slice(0, -1)} `;
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
buffer += current;
|
|
399
|
+
const text = buffer.trim();
|
|
400
|
+
buffer = "";
|
|
401
|
+
if (!text || text.startsWith("#"))
|
|
402
|
+
continue;
|
|
403
|
+
lines.push({ line: start, text });
|
|
404
|
+
}
|
|
405
|
+
return lines;
|
|
406
|
+
}
|
|
407
|
+
function parseFromInstruction(text) {
|
|
408
|
+
if (!/^FROM\b/i.test(text))
|
|
409
|
+
return undefined;
|
|
410
|
+
const parts = text.split(/\s+/);
|
|
411
|
+
let index = 1;
|
|
412
|
+
while (index < parts.length && parts[index].startsWith("--")) {
|
|
413
|
+
if (!parts[index].includes("="))
|
|
414
|
+
index += 1;
|
|
415
|
+
index += 1;
|
|
416
|
+
}
|
|
417
|
+
const image = parts[index];
|
|
418
|
+
if (!image)
|
|
419
|
+
return undefined;
|
|
420
|
+
const stage = /^as$/i.test(parts[index + 1] ?? "")
|
|
421
|
+
? parts[index + 2]
|
|
422
|
+
: undefined;
|
|
423
|
+
return stage ? { image, stage } : { image };
|
|
424
|
+
}
|
|
425
|
+
function imageIsUnpinned(image, stages) {
|
|
426
|
+
if (image.includes("${") ||
|
|
427
|
+
image.includes("@") ||
|
|
428
|
+
stages.has(image) ||
|
|
429
|
+
stages.has(image.toLowerCase()) ||
|
|
430
|
+
image.toLowerCase() === "scratch")
|
|
431
|
+
return false;
|
|
432
|
+
const slash = image.lastIndexOf("/");
|
|
433
|
+
const colon = image.lastIndexOf(":");
|
|
434
|
+
if (colon <= slash)
|
|
435
|
+
return true;
|
|
436
|
+
return image.slice(colon + 1) === "latest";
|
|
437
|
+
}
|
|
438
|
+
function parseUserName(text) {
|
|
439
|
+
if (!/^USER\b/i.test(text))
|
|
440
|
+
return undefined;
|
|
441
|
+
const parts = text
|
|
442
|
+
.split(/\s+/)
|
|
443
|
+
.slice(1)
|
|
444
|
+
.filter((part) => part && !part.startsWith("--"));
|
|
445
|
+
const raw = parts[0];
|
|
446
|
+
if (!raw)
|
|
447
|
+
return undefined;
|
|
448
|
+
return raw.replace(/^["']|["']$/g, "").split(":")[0];
|
|
449
|
+
}
|
|
450
|
+
function runPipesToShell(text) {
|
|
451
|
+
return (/^RUN\b/i.test(text) &&
|
|
452
|
+
/\b(?:curl|wget)\b/i.test(text) &&
|
|
453
|
+
/\|\s*(?:sudo\s+)?(?:\S*\/)?(?:ba)?sh\b/i.test(text));
|
|
454
|
+
}
|
|
455
|
+
function lineLocation(line, path) {
|
|
456
|
+
return { line, column: 1, path };
|
|
457
|
+
}
|
|
458
|
+
function parseDockerfile(content) {
|
|
459
|
+
const instructions = logicalDockerfileLines(content);
|
|
460
|
+
const froms = instructions.flatMap((instruction) => {
|
|
461
|
+
const parsed = parseFromInstruction(instruction.text);
|
|
462
|
+
return parsed ? [{ ...instruction, ...parsed }] : [];
|
|
463
|
+
});
|
|
464
|
+
if (!froms.length)
|
|
465
|
+
throw new ScanInputError("A Dockerfile requires a FROM instruction.");
|
|
466
|
+
const stages = new Set();
|
|
467
|
+
const resources = [];
|
|
468
|
+
froms.forEach((from, index) => {
|
|
469
|
+
resources.push({
|
|
470
|
+
id: `dockerfile.from.${index + 1}`,
|
|
471
|
+
type: "dockerfile:from",
|
|
472
|
+
value: { image: from.image, unpinned: imageIsUnpinned(from.image, stages) },
|
|
473
|
+
locate: () => lineLocation(from.line, "image"),
|
|
474
|
+
});
|
|
475
|
+
if (from.stage) {
|
|
476
|
+
stages.add(from.stage);
|
|
477
|
+
stages.add(from.stage.toLowerCase());
|
|
478
|
+
}
|
|
479
|
+
});
|
|
480
|
+
let lastUser;
|
|
481
|
+
for (const instruction of instructions) {
|
|
482
|
+
const name = parseUserName(instruction.text);
|
|
483
|
+
if (name !== undefined)
|
|
484
|
+
lastUser = { line: instruction.line, name };
|
|
485
|
+
}
|
|
486
|
+
const root = !lastUser || lastUser.name.toLowerCase() === "root" || lastUser.name === "0";
|
|
487
|
+
resources.push({
|
|
488
|
+
id: "dockerfile.user",
|
|
489
|
+
type: "dockerfile:user",
|
|
490
|
+
value: { user: lastUser?.name ?? "", root },
|
|
491
|
+
locate: () => lineLocation(lastUser?.line ?? froms[0].line, "user"),
|
|
492
|
+
});
|
|
493
|
+
instructions.forEach((instruction, index) => {
|
|
494
|
+
if (!/^RUN\b/i.test(instruction.text))
|
|
495
|
+
return;
|
|
496
|
+
resources.push({
|
|
497
|
+
id: `dockerfile.run.${index + 1}`,
|
|
498
|
+
type: "dockerfile:run",
|
|
499
|
+
value: { piped: runPipesToShell(instruction.text) },
|
|
500
|
+
locate: () => lineLocation(instruction.line, "run"),
|
|
501
|
+
});
|
|
502
|
+
});
|
|
503
|
+
return {
|
|
504
|
+
format: "dockerfile",
|
|
505
|
+
resources,
|
|
506
|
+
warnings: [submittedTextWarning],
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
function triggersPullRequestTarget(value) {
|
|
510
|
+
if (value === "pull_request_target")
|
|
511
|
+
return true;
|
|
512
|
+
if (Array.isArray(value))
|
|
513
|
+
return value.some((item) => item === "pull_request_target");
|
|
514
|
+
return Object.prototype.hasOwnProperty.call(object(value), "pull_request_target");
|
|
515
|
+
}
|
|
516
|
+
function workflowChecksOutHead(jobs) {
|
|
517
|
+
for (const job of Object.values(object(jobs))) {
|
|
518
|
+
for (const step of array(object(job).steps)) {
|
|
519
|
+
const inputs = object(object(step).with);
|
|
520
|
+
const text = [inputs.ref, inputs.sha]
|
|
521
|
+
.filter((item) => typeof item === "string")
|
|
522
|
+
.join("\n");
|
|
523
|
+
if (text.includes("github.event.pull_request.head"))
|
|
524
|
+
return true;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
return false;
|
|
528
|
+
}
|
|
529
|
+
function permissionsAreBroad(top, jobs, pullRequestTarget) {
|
|
530
|
+
const values = [
|
|
531
|
+
top,
|
|
532
|
+
...Object.values(object(jobs)).map((job) => object(job).permissions),
|
|
533
|
+
];
|
|
534
|
+
if (values.some((value) => value === "write-all"))
|
|
535
|
+
return true;
|
|
536
|
+
if (!pullRequestTarget)
|
|
537
|
+
return false;
|
|
538
|
+
return values.some((value) => object(value).contents === "write");
|
|
539
|
+
}
|
|
540
|
+
function parseGithubWorkflow(content) {
|
|
541
|
+
const lines = new LineCounter();
|
|
542
|
+
let documents;
|
|
543
|
+
try {
|
|
544
|
+
documents = parseAllDocuments(content, {
|
|
545
|
+
lineCounter: lines,
|
|
546
|
+
prettyErrors: false,
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
catch {
|
|
550
|
+
throw new ScanInputError("Invalid GitHub Actions workflow YAML.");
|
|
551
|
+
}
|
|
552
|
+
const resources = [];
|
|
553
|
+
let found = false;
|
|
554
|
+
for (const [documentIndex, doc] of documents.entries()) {
|
|
555
|
+
if (doc.errors.length)
|
|
556
|
+
throw new ScanInputError("Invalid GitHub Actions workflow YAML.");
|
|
557
|
+
let data;
|
|
558
|
+
try {
|
|
559
|
+
data = doc.toJS({ maxAliasCount: 0 });
|
|
560
|
+
}
|
|
561
|
+
catch {
|
|
562
|
+
throw new ScanInputError("YAML aliases are not supported. Expand anchors before scanning.");
|
|
563
|
+
}
|
|
564
|
+
if (data == null)
|
|
565
|
+
continue;
|
|
566
|
+
checkShape(data);
|
|
567
|
+
const root = object(data);
|
|
568
|
+
if (!("on" in root) || !("jobs" in root))
|
|
569
|
+
continue;
|
|
570
|
+
found = true;
|
|
571
|
+
const pullRequestTarget = triggersPullRequestTarget(root.on);
|
|
572
|
+
const checksOutHead = workflowChecksOutHead(root.jobs);
|
|
573
|
+
const broad = permissionsAreBroad(root.permissions, root.jobs, pullRequestTarget);
|
|
574
|
+
const position = (path) => {
|
|
575
|
+
let node = doc.getIn(path, true);
|
|
576
|
+
if (!node?.range)
|
|
577
|
+
node = doc.getIn(["on"], true);
|
|
578
|
+
const at = lines.linePos(node?.range?.[0] ?? doc.range?.[0] ?? 0);
|
|
579
|
+
return {
|
|
580
|
+
line: at.line,
|
|
581
|
+
column: at.col,
|
|
582
|
+
path: [documentIndex, ...path].join("."),
|
|
583
|
+
};
|
|
584
|
+
};
|
|
585
|
+
resources.push({
|
|
586
|
+
id: "workflow.on",
|
|
587
|
+
type: "github:trigger",
|
|
588
|
+
value: { pullRequestTarget, checksOutHead },
|
|
589
|
+
locate: () => position(["on"]),
|
|
590
|
+
});
|
|
591
|
+
resources.push({
|
|
592
|
+
id: "workflow.permissions",
|
|
593
|
+
type: "github:permissions",
|
|
594
|
+
value: { broad },
|
|
595
|
+
locate: () => position(root.permissions != null ? ["permissions"] : ["on"]),
|
|
596
|
+
});
|
|
597
|
+
for (const [jobName, job] of Object.entries(object(root.jobs))) {
|
|
598
|
+
array(object(job).steps).forEach((step, index) => {
|
|
599
|
+
const uses = object(step).uses;
|
|
600
|
+
if (typeof uses !== "string")
|
|
601
|
+
return;
|
|
602
|
+
const ref = uses.includes("@")
|
|
603
|
+
? uses.slice(uses.lastIndexOf("@") + 1)
|
|
604
|
+
: "";
|
|
605
|
+
resources.push({
|
|
606
|
+
id: `jobs.${jobName}.steps.${index}`,
|
|
607
|
+
type: "github:action",
|
|
608
|
+
value: { uses, floating: ref === "main" || ref === "master" },
|
|
609
|
+
locate: () => position(["jobs", jobName, "steps", index, "uses"]),
|
|
610
|
+
});
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
if (!found)
|
|
615
|
+
throw new ScanInputError("A GitHub Actions workflow requires top-level on and jobs keys.");
|
|
616
|
+
return {
|
|
617
|
+
format: "github",
|
|
618
|
+
resources,
|
|
619
|
+
warnings: [submittedTextWarning],
|
|
620
|
+
};
|
|
621
|
+
}
|
|
355
622
|
function composeServiceMap(root) {
|
|
356
623
|
if (root.apiVersion || root.kind || root.Resources || root.resources)
|
|
357
624
|
return undefined;
|
package/dist/iac/rules.js
CHANGED
|
@@ -197,6 +197,48 @@ export const rules = {
|
|
|
197
197
|
detail: "A role grants wildcard verbs, resources, or API groups.",
|
|
198
198
|
remediation: "Scope the role to the required API groups, resource names, and verbs.",
|
|
199
199
|
},
|
|
200
|
+
DF001: {
|
|
201
|
+
category: "security",
|
|
202
|
+
severity: "high",
|
|
203
|
+
title: "Dockerfile runs as root",
|
|
204
|
+
detail: "The last USER is root or 0, or the Dockerfile has no USER instruction.",
|
|
205
|
+
remediation: "Add a final USER instruction for a non-root account that the image supports.",
|
|
206
|
+
},
|
|
207
|
+
DF002: {
|
|
208
|
+
category: "security",
|
|
209
|
+
severity: "medium",
|
|
210
|
+
title: "Unpinned container image",
|
|
211
|
+
detail: "A FROM image uses the latest tag, or it has no tag and no digest.",
|
|
212
|
+
remediation: "Pin a release tag or an image digest. Scratch and named stages are not image pins.",
|
|
213
|
+
},
|
|
214
|
+
DF003: {
|
|
215
|
+
category: "security",
|
|
216
|
+
severity: "high",
|
|
217
|
+
title: "Remote script piped to a shell",
|
|
218
|
+
detail: "A RUN instruction downloads a script with curl or wget and pipes it to sh or bash.",
|
|
219
|
+
remediation: "Download the script, review it, and pin a checksum before running it. Do not pipe a remote script to a shell.",
|
|
220
|
+
},
|
|
221
|
+
GH001: {
|
|
222
|
+
category: "security",
|
|
223
|
+
severity: "critical",
|
|
224
|
+
title: "Pull request target checks out PR code",
|
|
225
|
+
detail: "The workflow triggers on pull_request_target and checks out the pull request head ref or head sha.",
|
|
226
|
+
remediation: "Avoid checking out pull request head code from pull_request_target. Use pull_request for untrusted code, or check out the base repository only.",
|
|
227
|
+
},
|
|
228
|
+
GH002: {
|
|
229
|
+
category: "security",
|
|
230
|
+
severity: "high",
|
|
231
|
+
title: "Workflow permissions are broad",
|
|
232
|
+
detail: "permissions is write-all, or a pull_request_target workflow sets contents to write.",
|
|
233
|
+
remediation: "Set the narrowest permissions the workflow needs. Keep contents read-only on pull_request_target.",
|
|
234
|
+
},
|
|
235
|
+
GH003: {
|
|
236
|
+
category: "security",
|
|
237
|
+
severity: "medium",
|
|
238
|
+
title: "Action ref is a floating branch",
|
|
239
|
+
detail: "A uses value is pinned to @main or @master.",
|
|
240
|
+
remediation: "Pin the action to a release tag or a full commit SHA.",
|
|
241
|
+
},
|
|
200
242
|
};
|
|
201
243
|
export function resourceFindings(resources, format) {
|
|
202
244
|
const findings = [];
|
|
@@ -212,6 +254,14 @@ export function resourceFindings(resources, format) {
|
|
|
212
254
|
location: resource.locate(path),
|
|
213
255
|
});
|
|
214
256
|
};
|
|
257
|
+
if (format === "dockerfile") {
|
|
258
|
+
checkDockerfile(resource, check);
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
if (format === "github") {
|
|
262
|
+
checkGithub(resource, check);
|
|
263
|
+
continue;
|
|
264
|
+
}
|
|
215
265
|
walk(resource.value, (key, value, path, parent) => {
|
|
216
266
|
if (typeof value !== "string")
|
|
217
267
|
return;
|
|
@@ -232,6 +282,23 @@ export function resourceFindings(resources, format) {
|
|
|
232
282
|
}
|
|
233
283
|
return { findings, checked };
|
|
234
284
|
}
|
|
285
|
+
function checkDockerfile(resource, check) {
|
|
286
|
+
if (resource.type === "dockerfile:user")
|
|
287
|
+
check("DF001", resource.value.root === true, ["user"]);
|
|
288
|
+
else if (resource.type === "dockerfile:from")
|
|
289
|
+
check("DF002", resource.value.unpinned === true, ["image"]);
|
|
290
|
+
else if (resource.type === "dockerfile:run")
|
|
291
|
+
check("DF003", resource.value.piped === true, ["run"]);
|
|
292
|
+
}
|
|
293
|
+
function checkGithub(resource, check) {
|
|
294
|
+
if (resource.type === "github:trigger")
|
|
295
|
+
check("GH001", resource.value.pullRequestTarget === true &&
|
|
296
|
+
resource.value.checksOutHead === true, ["on"]);
|
|
297
|
+
else if (resource.type === "github:permissions")
|
|
298
|
+
check("GH002", resource.value.broad === true, ["permissions"]);
|
|
299
|
+
else if (resource.type === "github:action")
|
|
300
|
+
check("GH003", resource.value.floating === true, ["uses"]);
|
|
301
|
+
}
|
|
235
302
|
function walk(value, visit, path = []) {
|
|
236
303
|
if (Array.isArray(value))
|
|
237
304
|
value.forEach((child, index) => walk(child, visit, [...path, index]));
|
package/dist/index.js
CHANGED
|
File without changes
|
package/dist/server.js
CHANGED
|
@@ -73,12 +73,14 @@ export function createServer(remote = false) {
|
|
|
73
73
|
"kubernetes",
|
|
74
74
|
"pulumi",
|
|
75
75
|
"compose",
|
|
76
|
+
"dockerfile",
|
|
77
|
+
"github",
|
|
76
78
|
"application",
|
|
77
79
|
"auto",
|
|
78
80
|
])
|
|
79
81
|
.optional()
|
|
80
82
|
.default("auto")
|
|
81
|
-
.describe("Content format: terraform, cloudformation, kubernetes, pulumi, compose, application, auto"),
|
|
83
|
+
.describe("Content format: terraform, cloudformation, kubernetes, pulumi, compose, application, auto. Includes Dockerfile and GitHub Actions."),
|
|
82
84
|
},
|
|
83
85
|
}, async ({ content, focus, format, minSeverity, maxFindings }) => {
|
|
84
86
|
const botHints = fetchPublicBotHints();
|
|
@@ -126,7 +128,7 @@ export function createServer(remote = false) {
|
|
|
126
128
|
description: (name === "scan_iac_stub"
|
|
127
129
|
? "Compatibility alias for scan_iac; runs the full scanner. "
|
|
128
130
|
: "") +
|
|
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.",
|
|
131
|
+
"Parse Terraform HCL/JSON, CloudFormation YAML/JSON, Kubernetes manifests, Pulumi YAML, Pulumi programs, or Docker Compose. Includes Dockerfile and GitHub Actions. Check security, reliability and cost rules with resource locations and remediation. Pulumi programs are scanned as text.",
|
|
130
132
|
annotations: {
|
|
131
133
|
readOnlyHint: true,
|
|
132
134
|
destructiveHint: false,
|
|
@@ -134,7 +136,7 @@ export function createServer(remote = false) {
|
|
|
134
136
|
},
|
|
135
137
|
_meta: { securitySchemes: [{ type: "oauth2", scopes: ["mcp"] }] },
|
|
136
138
|
inputSchema: {
|
|
137
|
-
content: contentSchema.describe("Terraform HCL/JSON, CloudFormation YAML/JSON, Kubernetes YAML/JSON, Pulumi YAML or program text, or Docker Compose"),
|
|
139
|
+
content: contentSchema.describe("Terraform HCL/JSON, CloudFormation YAML/JSON, Kubernetes YAML/JSON, Pulumi YAML or program text, or Docker Compose. Includes Dockerfile and GitHub Actions."),
|
|
138
140
|
format: z
|
|
139
141
|
.enum([
|
|
140
142
|
"terraform",
|
|
@@ -142,6 +144,8 @@ export function createServer(remote = false) {
|
|
|
142
144
|
"kubernetes",
|
|
143
145
|
"pulumi",
|
|
144
146
|
"compose",
|
|
147
|
+
"dockerfile",
|
|
148
|
+
"github",
|
|
145
149
|
"auto",
|
|
146
150
|
])
|
|
147
151
|
.default("auto"),
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.6.
|
|
1
|
+
export declare const VERSION = "0.6.9";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.6.
|
|
1
|
+
export const VERSION = "0.6.9";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@skaleagents/swarm",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.9",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -15,6 +15,10 @@
|
|
|
15
15
|
"./scan": {
|
|
16
16
|
"types": "./dist/iac/scan.d.ts",
|
|
17
17
|
"import": "./dist/iac/scan.js"
|
|
18
|
+
},
|
|
19
|
+
"./review": {
|
|
20
|
+
"types": "./dist/review.d.ts",
|
|
21
|
+
"import": "./dist/review.js"
|
|
18
22
|
}
|
|
19
23
|
},
|
|
20
24
|
"bin": {
|