@skaleagents/swarm 0.6.10 → 0.6.11
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 +3 -1
- package/dist/iac/parse.d.ts +4 -1
- package/dist/iac/parse.js +238 -4
- package/dist/iac/rules.js +42 -0
- package/dist/server.js +7 -3
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -62,7 +62,9 @@ review-brief builder.
|
|
|
62
62
|
## Infrastructure scanning
|
|
63
63
|
|
|
64
64
|
`scan_iac` parses Terraform HCL/JSON, CloudFormation YAML/JSON, and Kubernetes
|
|
65
|
-
manifests, including multi-document YAML and Kubernetes Lists. It
|
|
65
|
+
manifests, including multi-document YAML and Kubernetes Lists. It also reads
|
|
66
|
+
Dockerfiles, GitHub Actions workflows, Helm templates, Ansible playbooks,
|
|
67
|
+
Bicep files, and ARM templates. It returns a
|
|
66
68
|
resource inventory and findings with stable rule IDs, severity, property paths,
|
|
67
69
|
line locations, and remediation. Findings never include matched secret values.
|
|
68
70
|
|
package/dist/iac/parse.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type IacFormat = "terraform" | "cloudformation" | "kubernetes" | "pulumi" | "compose" | "dockerfile" | "github" | "helm" | "ansible";
|
|
1
|
+
export type IacFormat = "terraform" | "cloudformation" | "kubernetes" | "pulumi" | "compose" | "dockerfile" | "github" | "helm" | "ansible" | "bicep" | "arm";
|
|
2
2
|
export type Path = (string | number)[];
|
|
3
3
|
export type Location = {
|
|
4
4
|
line: number;
|
|
@@ -28,5 +28,8 @@ export declare function looksLikeDockerfile(content: string): boolean;
|
|
|
28
28
|
export declare function looksLikeAnsible(content: string): boolean;
|
|
29
29
|
export declare function looksLikeHelm(content: string): boolean;
|
|
30
30
|
export declare function looksLikeGithubWorkflow(content: string): boolean;
|
|
31
|
+
export declare function looksLikeTerraformHcl(content: string): boolean;
|
|
32
|
+
export declare function looksLikeBicep(content: string): boolean;
|
|
33
|
+
export declare function looksLikeArm(content: string): boolean;
|
|
31
34
|
export declare function looksLikePulumiYaml(content: string): boolean;
|
|
32
35
|
export declare function parseIac(content: string, requested?: IacFormat | "auto"): ParsedIac;
|
package/dist/iac/parse.js
CHANGED
|
@@ -44,7 +44,9 @@ export function looksLikeIac(content) {
|
|
|
44
44
|
looksLikeDockerfile(content) ||
|
|
45
45
|
looksLikeGithubWorkflow(content) ||
|
|
46
46
|
looksLikeHelm(content) ||
|
|
47
|
-
looksLikeAnsible(content)
|
|
47
|
+
looksLikeAnsible(content) ||
|
|
48
|
+
looksLikeBicep(content) ||
|
|
49
|
+
looksLikeArm(content));
|
|
48
50
|
}
|
|
49
51
|
export function looksLikePulumiProgram(content) {
|
|
50
52
|
return /(?:^|\n)\s*(?:import\s+\*\s+as\s+pulumi\b|import\s+pulumi\b|from\s+["']@pulumi\/)/.test(content);
|
|
@@ -89,6 +91,44 @@ export function looksLikeGithubWorkflow(content) {
|
|
|
89
91
|
return false;
|
|
90
92
|
return topOn && topLevelKey(content, "jobs");
|
|
91
93
|
}
|
|
94
|
+
export function looksLikeTerraformHcl(content) {
|
|
95
|
+
return /^\s*(?:(?:resource|module|variable|provider|data|output)\s+"|(?:terraform|locals)\s*\{)/m.test(content);
|
|
96
|
+
}
|
|
97
|
+
export function looksLikeBicep(content) {
|
|
98
|
+
const visible = maskIaCComments(content);
|
|
99
|
+
if (looksLikeTerraformHcl(visible))
|
|
100
|
+
return false;
|
|
101
|
+
return /^\s*resource\s+[A-Za-z_][A-Za-z0-9_]*\s+['"]Microsoft\./m.test(visible);
|
|
102
|
+
}
|
|
103
|
+
export function looksLikeArm(content) {
|
|
104
|
+
const visible = maskIaCComments(content).trim();
|
|
105
|
+
if (!visible.startsWith("{"))
|
|
106
|
+
return false;
|
|
107
|
+
let value;
|
|
108
|
+
try {
|
|
109
|
+
value = JSON.parse(visible);
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
const root = object(value);
|
|
115
|
+
if (typeof root.AWSTemplateFormatVersion === "string")
|
|
116
|
+
return false;
|
|
117
|
+
if (Object.values(object(root.Resources)).some((item) => String(object(item).Type ?? "").startsWith("AWS::")))
|
|
118
|
+
return false;
|
|
119
|
+
if ("resource" in root && !Array.isArray(root.resources))
|
|
120
|
+
return false;
|
|
121
|
+
if ("format_version" in root ||
|
|
122
|
+
"planned_values" in root ||
|
|
123
|
+
"resource_changes" in root)
|
|
124
|
+
return false;
|
|
125
|
+
if (typeof root.$schema === "string" &&
|
|
126
|
+
root.$schema.includes("deploymentTemplate"))
|
|
127
|
+
return true;
|
|
128
|
+
if (typeof root.contentVersion !== "string" || !Array.isArray(root.resources))
|
|
129
|
+
return false;
|
|
130
|
+
return root.resources.some((item) => String(object(item).type ?? "").startsWith("Microsoft."));
|
|
131
|
+
}
|
|
92
132
|
export function looksLikePulumiYaml(content) {
|
|
93
133
|
return (/^\s*resources\s*:/m.test(content) &&
|
|
94
134
|
/^\s+type\s*:\s*[A-Za-z0-9_.-]+:\S+/m.test(content));
|
|
@@ -114,6 +154,16 @@ export function parseIac(content, requested = "auto") {
|
|
|
114
154
|
(requested === "auto" && looksLikeHelm(content))) {
|
|
115
155
|
return parseHelm(content);
|
|
116
156
|
}
|
|
157
|
+
if (requested === "bicep" || (requested === "auto" && looksLikeBicep(content))) {
|
|
158
|
+
if (!looksLikeBicep(content))
|
|
159
|
+
throw new ScanInputError("A Bicep file requires a Microsoft resource declaration.");
|
|
160
|
+
return parseAzure(content, "bicep");
|
|
161
|
+
}
|
|
162
|
+
if (requested === "arm" || (requested === "auto" && looksLikeArm(content))) {
|
|
163
|
+
if (!looksLikeArm(content))
|
|
164
|
+
throw new ScanInputError("An ARM template requires a deploymentTemplate schema or contentVersion with Microsoft resources.");
|
|
165
|
+
return parseAzure(content, "arm");
|
|
166
|
+
}
|
|
117
167
|
if (requested !== "terraform" &&
|
|
118
168
|
requested !== "compose" &&
|
|
119
169
|
looksLikePulumiProgram(content) &&
|
|
@@ -123,8 +173,7 @@ export function parseIac(content, requested = "auto") {
|
|
|
123
173
|
const warnings = [];
|
|
124
174
|
const resources = [];
|
|
125
175
|
const isHcl = (requested === "terraform" && !content.trimStart().startsWith("{")) ||
|
|
126
|
-
(requested === "auto" &&
|
|
127
|
-
/^\s*(?:(?:resource|module|variable|provider|data|output)\s+"|(?:terraform|locals)\s*\{)/m.test(content));
|
|
176
|
+
(requested === "auto" && looksLikeTerraformHcl(content));
|
|
128
177
|
if (isHcl) {
|
|
129
178
|
let data;
|
|
130
179
|
try {
|
|
@@ -330,7 +379,7 @@ export function parseIac(content, requested = "auto") {
|
|
|
330
379
|
}
|
|
331
380
|
}
|
|
332
381
|
if (!format)
|
|
333
|
-
throw new ScanInputError("No IaC document found. Choose Terraform, CloudFormation, Kubernetes, Pulumi, Docker Compose, a Dockerfile, a GitHub Actions workflow, a Helm template, or an
|
|
382
|
+
throw new ScanInputError("No IaC document found. Choose Terraform, CloudFormation, Kubernetes, Pulumi, Docker Compose, a Dockerfile, a GitHub Actions workflow, a Helm template, an Ansible playbook, Bicep, or an ARM template.");
|
|
334
383
|
if (!resources.length)
|
|
335
384
|
warnings.push("No resources found in the submitted document.");
|
|
336
385
|
if (/(?:!(?:Ref|Sub|GetAtt|If)\b|"(?:Ref|Fn::\w+)"\s*:|\$\{)/.test(content))
|
|
@@ -517,6 +566,53 @@ function sourceIsSecret(source) {
|
|
|
517
566
|
return true;
|
|
518
567
|
return base === "credentials";
|
|
519
568
|
}
|
|
569
|
+
function secretVariableName(name) {
|
|
570
|
+
const normalized = name.replace(/-/g, "_");
|
|
571
|
+
return (/(?:^|_)(?:password|passwd|secret|token)$/i.test(normalized) ||
|
|
572
|
+
/(?:^|_)(?:api_?key|auth_token|access_key|private_key)$/i.test(normalized));
|
|
573
|
+
}
|
|
574
|
+
function isShellReference(value) {
|
|
575
|
+
return (/^\$[A-Za-z_][A-Za-z0-9_]*$/.test(value) ||
|
|
576
|
+
/^\$\{[A-Za-z_][A-Za-z0-9_]*\}$/.test(value));
|
|
577
|
+
}
|
|
578
|
+
function parseImageAssignments(kind, rest) {
|
|
579
|
+
const trimmed = rest.trim();
|
|
580
|
+
if (!trimmed)
|
|
581
|
+
return [];
|
|
582
|
+
if (kind === "ARG" && /^[A-Za-z_][A-Za-z0-9_]*$/.test(trimmed))
|
|
583
|
+
return [{ name: trimmed, value: "" }];
|
|
584
|
+
if (!trimmed.includes("=")) {
|
|
585
|
+
const spaced = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)\s+([\s\S]*)$/);
|
|
586
|
+
if (!spaced)
|
|
587
|
+
return [];
|
|
588
|
+
return [{ name: spaced[1], value: unquoteAssignment(spaced[2].trim()) }];
|
|
589
|
+
}
|
|
590
|
+
const pairs = [];
|
|
591
|
+
const pattern = /([A-Za-z_][A-Za-z0-9_]*)=(?:"([^"]*)"|'([^']*)'|([^\s]*))/g;
|
|
592
|
+
for (const match of trimmed.matchAll(pattern)) {
|
|
593
|
+
pairs.push({
|
|
594
|
+
name: match[1],
|
|
595
|
+
value: match[2] ?? match[3] ?? match[4] ?? "",
|
|
596
|
+
});
|
|
597
|
+
}
|
|
598
|
+
return pairs;
|
|
599
|
+
}
|
|
600
|
+
function unquoteAssignment(value) {
|
|
601
|
+
if ((value.startsWith('"') && value.endsWith('"') && value.length >= 2) ||
|
|
602
|
+
(value.startsWith("'") && value.endsWith("'") && value.length >= 2))
|
|
603
|
+
return value.slice(1, -1);
|
|
604
|
+
return value;
|
|
605
|
+
}
|
|
606
|
+
function workflowEnvIsLiteral(value) {
|
|
607
|
+
const trimmed = value.trim();
|
|
608
|
+
if (!trimmed)
|
|
609
|
+
return false;
|
|
610
|
+
if (/^\$\{\{\s*secrets\.[^}]+\}\}$/i.test(trimmed))
|
|
611
|
+
return false;
|
|
612
|
+
if (/^\$\{\{\s*github\.token\s*\}\}$/i.test(trimmed))
|
|
613
|
+
return false;
|
|
614
|
+
return true;
|
|
615
|
+
}
|
|
520
616
|
function lineLocation(line, path) {
|
|
521
617
|
return { line, column: 1, path };
|
|
522
618
|
}
|
|
@@ -576,6 +672,22 @@ function parseDockerfile(content) {
|
|
|
576
672
|
locate: () => lineLocation(instruction.line, "copy"),
|
|
577
673
|
});
|
|
578
674
|
});
|
|
675
|
+
instructions.forEach((instruction, index) => {
|
|
676
|
+
const header = instruction.text.match(/^(ENV|ARG)\b\s*([\s\S]*)$/i);
|
|
677
|
+
if (!header)
|
|
678
|
+
return;
|
|
679
|
+
const kind = header[1].toUpperCase() === "ARG" ? "ARG" : "ENV";
|
|
680
|
+
const assignments = parseImageAssignments(kind, header[2] ?? "");
|
|
681
|
+
const literal = assignments.some((item) => secretVariableName(item.name) &&
|
|
682
|
+
item.value.trim() !== "" &&
|
|
683
|
+
!isShellReference(item.value.trim()));
|
|
684
|
+
resources.push({
|
|
685
|
+
id: `dockerfile.env.${index + 1}`,
|
|
686
|
+
type: "dockerfile:env",
|
|
687
|
+
value: { literal },
|
|
688
|
+
locate: () => lineLocation(instruction.line, "env"),
|
|
689
|
+
});
|
|
690
|
+
});
|
|
579
691
|
return {
|
|
580
692
|
format: "dockerfile",
|
|
581
693
|
resources,
|
|
@@ -678,7 +790,27 @@ function parseGithubWorkflow(content) {
|
|
|
678
790
|
value: { broad },
|
|
679
791
|
locate: () => position(root.permissions != null ? ["permissions"] : ["on"]),
|
|
680
792
|
});
|
|
793
|
+
const addEnv = (envValue, path) => {
|
|
794
|
+
for (const [key, raw] of Object.entries(object(envValue))) {
|
|
795
|
+
if (typeof raw !== "string" && raw != null)
|
|
796
|
+
continue;
|
|
797
|
+
if (!secretVariableName(key))
|
|
798
|
+
continue;
|
|
799
|
+
const value = typeof raw === "string" ? raw : "";
|
|
800
|
+
resources.push({
|
|
801
|
+
id: `env.${[...path, key].join(".")}`,
|
|
802
|
+
type: "github:env",
|
|
803
|
+
value: { literal: workflowEnvIsLiteral(value) },
|
|
804
|
+
locate: () => position([...path, key]),
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
};
|
|
808
|
+
addEnv(root.env, ["env"]);
|
|
681
809
|
for (const [jobName, job] of Object.entries(object(root.jobs))) {
|
|
810
|
+
addEnv(object(job).env, ["jobs", jobName, "env"]);
|
|
811
|
+
array(object(job).steps).forEach((step, index) => {
|
|
812
|
+
addEnv(object(step).env, ["jobs", jobName, "steps", index, "env"]);
|
|
813
|
+
});
|
|
682
814
|
array(object(job).steps).forEach((step, index) => {
|
|
683
815
|
const uses = object(step).uses;
|
|
684
816
|
if (typeof uses !== "string")
|
|
@@ -836,3 +968,105 @@ function parseAnsible(content) {
|
|
|
836
968
|
warnings: [ansibleFactsWarning],
|
|
837
969
|
};
|
|
838
970
|
}
|
|
971
|
+
function maskIaCComments(content) {
|
|
972
|
+
const withoutBlock = content.replace(/\/\*[\s\S]*?\*\//g, (match) => match.replace(/[^\n]/g, " "));
|
|
973
|
+
return withoutBlock
|
|
974
|
+
.split("\n")
|
|
975
|
+
.map((line) => {
|
|
976
|
+
let out = "";
|
|
977
|
+
let quote;
|
|
978
|
+
for (let index = 0; index < line.length; index++) {
|
|
979
|
+
const char = line[index];
|
|
980
|
+
const next = line[index + 1];
|
|
981
|
+
if (quote) {
|
|
982
|
+
out += char;
|
|
983
|
+
if (char === "\\" && quote === '"') {
|
|
984
|
+
if (next)
|
|
985
|
+
out += next;
|
|
986
|
+
index += 1;
|
|
987
|
+
continue;
|
|
988
|
+
}
|
|
989
|
+
if (char === quote)
|
|
990
|
+
quote = undefined;
|
|
991
|
+
continue;
|
|
992
|
+
}
|
|
993
|
+
if (char === '"' || char === "'") {
|
|
994
|
+
quote = char;
|
|
995
|
+
out += char;
|
|
996
|
+
continue;
|
|
997
|
+
}
|
|
998
|
+
if ((char === "/" && next === "/") ||
|
|
999
|
+
char === "#") {
|
|
1000
|
+
return out + " ".repeat(line.length - out.length);
|
|
1001
|
+
}
|
|
1002
|
+
out += char;
|
|
1003
|
+
}
|
|
1004
|
+
return out;
|
|
1005
|
+
})
|
|
1006
|
+
.join("\n");
|
|
1007
|
+
}
|
|
1008
|
+
function addressValueIsOpen(after) {
|
|
1009
|
+
return /(?:^|[\s\[,])(?:"\*"|"0\.0\.0\.0\/0"|'\*'|'0\.0\.0\.0\/0'|\*|0\.0\.0\.0\/0)(?=$|[\s,\]}"'])/.test(after);
|
|
1010
|
+
}
|
|
1011
|
+
function nearestScopeIsEgress(content, index) {
|
|
1012
|
+
const windowStart = Math.max(0, index - 800);
|
|
1013
|
+
const windowEnd = Math.min(content.length, index + 400);
|
|
1014
|
+
const slice = content.slice(windowStart, windowEnd);
|
|
1015
|
+
const relative = index - windowStart;
|
|
1016
|
+
let nearest;
|
|
1017
|
+
for (const match of slice.matchAll(/\b(Inbound|Outbound|ingress|egress)\b/gi)) {
|
|
1018
|
+
const at = match.index ?? 0;
|
|
1019
|
+
const distance = Math.abs(at - relative);
|
|
1020
|
+
const egress = /^(?:outbound|egress)$/i.test(match[1] ?? "");
|
|
1021
|
+
if (!nearest || distance < nearest.distance)
|
|
1022
|
+
nearest = { distance, egress };
|
|
1023
|
+
}
|
|
1024
|
+
return nearest?.egress ?? false;
|
|
1025
|
+
}
|
|
1026
|
+
function firstOpenAddress(content) {
|
|
1027
|
+
const pattern = /(source|destination)AddressPrefix(es)?["']?\s*[:=]/gi;
|
|
1028
|
+
for (const match of content.matchAll(pattern)) {
|
|
1029
|
+
const kind = (match[1] ?? "").toLowerCase();
|
|
1030
|
+
const index = match.index ?? 0;
|
|
1031
|
+
const after = content.slice(index + match[0].length, index + match[0].length + 240);
|
|
1032
|
+
if (!addressValueIsOpen(after))
|
|
1033
|
+
continue;
|
|
1034
|
+
if (kind === "destination" && nearestScopeIsEgress(content, index))
|
|
1035
|
+
continue;
|
|
1036
|
+
return { present: true, line: lineAt(content, index) };
|
|
1037
|
+
}
|
|
1038
|
+
return { present: false, line: 1 };
|
|
1039
|
+
}
|
|
1040
|
+
function enclosingAzureType(content, index) {
|
|
1041
|
+
const before = content.slice(Math.max(0, index - 4000), index);
|
|
1042
|
+
const declarations = [
|
|
1043
|
+
...before.matchAll(/resource\s+[A-Za-z_][\w]*\s+['"](Microsoft\.[^'"]+)['"]|(?:["']type["']|type)\s*:\s*["'](Microsoft\.[^"']+)["']/g),
|
|
1044
|
+
];
|
|
1045
|
+
const last = declarations.at(-1);
|
|
1046
|
+
return last?.[1] ?? last?.[2] ?? "";
|
|
1047
|
+
}
|
|
1048
|
+
function firstPublicNetwork(content) {
|
|
1049
|
+
const pattern = /publicNetworkAccess["']?\s*[:=]\s*(["']?)(Enabled|true|Disabled|false)\1(?![\w.-])/gi;
|
|
1050
|
+
for (const match of content.matchAll(pattern)) {
|
|
1051
|
+
if (!/^(?:enabled|true)$/i.test(match[2] ?? ""))
|
|
1052
|
+
continue;
|
|
1053
|
+
const index = match.index ?? 0;
|
|
1054
|
+
const type = enclosingAzureType(content, index);
|
|
1055
|
+
if (!/(?:database|storage|server)/i.test(type))
|
|
1056
|
+
continue;
|
|
1057
|
+
return { present: true, line: lineAt(content, index) };
|
|
1058
|
+
}
|
|
1059
|
+
return { present: false, line: 1 };
|
|
1060
|
+
}
|
|
1061
|
+
const azureExpressionWarning = "Bicep modules and ARM expressions are not evaluated.";
|
|
1062
|
+
function parseAzure(content, format) {
|
|
1063
|
+
const visible = maskIaCComments(content);
|
|
1064
|
+
return {
|
|
1065
|
+
format,
|
|
1066
|
+
resources: [
|
|
1067
|
+
literalResource("azure.exposure", "azure:exposure", firstOpenAddress(visible), "sourceAddressPrefix"),
|
|
1068
|
+
literalResource("azure.publicNetwork", "azure:publicNetwork", firstPublicNetwork(visible), "publicNetworkAccess"),
|
|
1069
|
+
],
|
|
1070
|
+
warnings: [azureExpressionWarning],
|
|
1071
|
+
};
|
|
1072
|
+
}
|
package/dist/iac/rules.js
CHANGED
|
@@ -253,6 +253,34 @@ export const rules = {
|
|
|
253
253
|
detail: "A run script echoes or prints a secrets expression.",
|
|
254
254
|
remediation: "Remove the printed secret from the script. Pass secrets through an environment variable that the step does not print.",
|
|
255
255
|
},
|
|
256
|
+
DF005: {
|
|
257
|
+
category: "security",
|
|
258
|
+
severity: "high",
|
|
259
|
+
title: "Secret assigned in an image variable",
|
|
260
|
+
detail: "A Dockerfile ENV or ARG assigns a non-empty literal to a secret-like name.",
|
|
261
|
+
remediation: "Pass the value at runtime or from a secret mount. Do not bake a literal secret into ENV or ARG.",
|
|
262
|
+
},
|
|
263
|
+
GH005: {
|
|
264
|
+
category: "security",
|
|
265
|
+
severity: "high",
|
|
266
|
+
title: "Secret assigned in workflow env",
|
|
267
|
+
detail: "A workflow env mapping assigns a non-empty literal to a secret-like name.",
|
|
268
|
+
remediation: "Reference secrets or github.token in the env value. Do not commit a literal secret.",
|
|
269
|
+
},
|
|
270
|
+
AZ001: {
|
|
271
|
+
category: "security",
|
|
272
|
+
severity: "high",
|
|
273
|
+
title: "Broad network exposure",
|
|
274
|
+
detail: "A source address prefix allows every address, or a destination prefix is open outside an egress scope.",
|
|
275
|
+
remediation: "Restrict source address prefixes to approved ranges. Keep open destination prefixes on egress rules only.",
|
|
276
|
+
},
|
|
277
|
+
AZ002: {
|
|
278
|
+
category: "security",
|
|
279
|
+
severity: "high",
|
|
280
|
+
title: "Database or storage allows public network access",
|
|
281
|
+
detail: "publicNetworkAccess is Enabled or true on a database, storage, or server resource.",
|
|
282
|
+
remediation: "Set publicNetworkAccess to Disabled and reach the resource through a private endpoint.",
|
|
283
|
+
},
|
|
256
284
|
HELM001: {
|
|
257
285
|
category: "security",
|
|
258
286
|
severity: "critical",
|
|
@@ -326,6 +354,10 @@ export function resourceFindings(resources, format) {
|
|
|
326
354
|
checkAnsible(resource, check);
|
|
327
355
|
continue;
|
|
328
356
|
}
|
|
357
|
+
if (format === "bicep" || format === "arm") {
|
|
358
|
+
checkAzure(resource, check);
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
329
361
|
walk(resource.value, (key, value, path, parent) => {
|
|
330
362
|
if (typeof value !== "string")
|
|
331
363
|
return;
|
|
@@ -355,6 +387,8 @@ function checkDockerfile(resource, check) {
|
|
|
355
387
|
check("DF003", resource.value.piped === true, ["run"]);
|
|
356
388
|
else if (resource.type === "dockerfile:copy")
|
|
357
389
|
check("DF004", resource.value.secret === true, ["copy"]);
|
|
390
|
+
else if (resource.type === "dockerfile:env")
|
|
391
|
+
check("DF005", resource.value.literal === true, ["env"]);
|
|
358
392
|
}
|
|
359
393
|
function checkGithub(resource, check) {
|
|
360
394
|
if (resource.type === "github:trigger")
|
|
@@ -366,6 +400,8 @@ function checkGithub(resource, check) {
|
|
|
366
400
|
check("GH003", resource.value.floating === true, ["uses"]);
|
|
367
401
|
else if (resource.type === "github:run")
|
|
368
402
|
check("GH004", resource.value.printsSecret === true, ["run"]);
|
|
403
|
+
else if (resource.type === "github:env")
|
|
404
|
+
check("GH005", resource.value.literal === true, ["env"]);
|
|
369
405
|
}
|
|
370
406
|
function checkHelm(resource, check) {
|
|
371
407
|
if (resource.type === "helm:privileged")
|
|
@@ -383,6 +419,12 @@ function checkAnsible(resource, check) {
|
|
|
383
419
|
else if (resource.type === "ansible:cidr")
|
|
384
420
|
check("ANS002", resource.value.present === true, ["cidr"]);
|
|
385
421
|
}
|
|
422
|
+
function checkAzure(resource, check) {
|
|
423
|
+
if (resource.type === "azure:exposure")
|
|
424
|
+
check("AZ001", resource.value.present === true, ["sourceAddressPrefix"]);
|
|
425
|
+
else if (resource.type === "azure:publicNetwork")
|
|
426
|
+
check("AZ002", resource.value.present === true, ["publicNetworkAccess"]);
|
|
427
|
+
}
|
|
386
428
|
function walk(value, visit, path = []) {
|
|
387
429
|
if (Array.isArray(value))
|
|
388
430
|
value.forEach((child, index) => walk(child, visit, [...path, index]));
|
package/dist/server.js
CHANGED
|
@@ -77,12 +77,14 @@ export function createServer(remote = false) {
|
|
|
77
77
|
"github",
|
|
78
78
|
"helm",
|
|
79
79
|
"ansible",
|
|
80
|
+
"bicep",
|
|
81
|
+
"arm",
|
|
80
82
|
"application",
|
|
81
83
|
"auto",
|
|
82
84
|
])
|
|
83
85
|
.optional()
|
|
84
86
|
.default("auto")
|
|
85
|
-
.describe("Content format: terraform, cloudformation, kubernetes, pulumi, compose, dockerfile, github, helm, ansible, application, auto."),
|
|
87
|
+
.describe("Content format: terraform, cloudformation, kubernetes, pulumi, compose, dockerfile, github, helm, ansible, bicep, arm, application, auto."),
|
|
86
88
|
},
|
|
87
89
|
}, async ({ content, focus, format, minSeverity, maxFindings }) => {
|
|
88
90
|
const botHints = fetchPublicBotHints();
|
|
@@ -130,7 +132,7 @@ export function createServer(remote = false) {
|
|
|
130
132
|
description: (name === "scan_iac_stub"
|
|
131
133
|
? "Compatibility alias for scan_iac; runs the full scanner. "
|
|
132
134
|
: "") +
|
|
133
|
-
"Parse Terraform HCL/JSON, CloudFormation YAML/JSON, Kubernetes manifests, Pulumi YAML, Pulumi programs, or Docker Compose. Includes Dockerfile, GitHub Actions, Helm templates, and
|
|
135
|
+
"Parse Terraform HCL/JSON, CloudFormation YAML/JSON, Kubernetes manifests, Pulumi YAML, Pulumi programs, or Docker Compose. Includes Dockerfile, GitHub Actions, Helm templates, Ansible playbooks, Bicep files, and ARM templates. Helm templates, Ansible facts, Bicep modules, and ARM expressions are not executed. Check security, reliability and cost rules with resource locations and remediation. Pulumi programs are scanned as text.",
|
|
134
136
|
annotations: {
|
|
135
137
|
readOnlyHint: true,
|
|
136
138
|
destructiveHint: false,
|
|
@@ -138,7 +140,7 @@ export function createServer(remote = false) {
|
|
|
138
140
|
},
|
|
139
141
|
_meta: { securitySchemes: [{ type: "oauth2", scopes: ["mcp"] }] },
|
|
140
142
|
inputSchema: {
|
|
141
|
-
content: contentSchema.describe("Terraform HCL/JSON, CloudFormation YAML/JSON, Kubernetes YAML/JSON, Pulumi YAML or program text, or Docker Compose. Includes Dockerfile, GitHub Actions, Helm templates, and
|
|
143
|
+
content: contentSchema.describe("Terraform HCL/JSON, CloudFormation YAML/JSON, Kubernetes YAML/JSON, Pulumi YAML or program text, or Docker Compose. Includes Dockerfile, GitHub Actions, Helm templates, Ansible playbooks, Bicep files, and ARM templates."),
|
|
142
144
|
format: z
|
|
143
145
|
.enum([
|
|
144
146
|
"terraform",
|
|
@@ -150,6 +152,8 @@ export function createServer(remote = false) {
|
|
|
150
152
|
"github",
|
|
151
153
|
"helm",
|
|
152
154
|
"ansible",
|
|
155
|
+
"bicep",
|
|
156
|
+
"arm",
|
|
153
157
|
"auto",
|
|
154
158
|
])
|
|
155
159
|
.default("auto"),
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.6.
|
|
1
|
+
export declare const VERSION = "0.6.11";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.6.
|
|
1
|
+
export const VERSION = "0.6.11";
|