@skaleagents/swarm 0.6.10 → 0.6.12
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 +540 -7
- package/dist/iac/rules.js +132 -0
- package/dist/server.js +15 -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" | "gitlab" | "azure-pipelines" | "cloudbuild" | "tfvars";
|
|
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,11 @@ 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) ||
|
|
50
|
+
looksLikePipeline(content) !== undefined ||
|
|
51
|
+
looksLikeTfvars(content));
|
|
48
52
|
}
|
|
49
53
|
export function looksLikePulumiProgram(content) {
|
|
50
54
|
return /(?:^|\n)\s*(?:import\s+\*\s+as\s+pulumi\b|import\s+pulumi\b|from\s+["']@pulumi\/)/.test(content);
|
|
@@ -89,6 +93,44 @@ export function looksLikeGithubWorkflow(content) {
|
|
|
89
93
|
return false;
|
|
90
94
|
return topOn && topLevelKey(content, "jobs");
|
|
91
95
|
}
|
|
96
|
+
export function looksLikeTerraformHcl(content) {
|
|
97
|
+
return /^\s*(?:(?:resource|module|variable|provider|data|output)\s+"|(?:terraform|locals)\s*\{)/m.test(content);
|
|
98
|
+
}
|
|
99
|
+
export function looksLikeBicep(content) {
|
|
100
|
+
const visible = maskIaCComments(content);
|
|
101
|
+
if (looksLikeTerraformHcl(visible))
|
|
102
|
+
return false;
|
|
103
|
+
return /^\s*resource\s+[A-Za-z_][A-Za-z0-9_]*\s+['"]Microsoft\./m.test(visible);
|
|
104
|
+
}
|
|
105
|
+
export function looksLikeArm(content) {
|
|
106
|
+
const visible = maskIaCComments(content).trim();
|
|
107
|
+
if (!visible.startsWith("{"))
|
|
108
|
+
return false;
|
|
109
|
+
let value;
|
|
110
|
+
try {
|
|
111
|
+
value = JSON.parse(visible);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
const root = object(value);
|
|
117
|
+
if (typeof root.AWSTemplateFormatVersion === "string")
|
|
118
|
+
return false;
|
|
119
|
+
if (Object.values(object(root.Resources)).some((item) => String(object(item).Type ?? "").startsWith("AWS::")))
|
|
120
|
+
return false;
|
|
121
|
+
if ("resource" in root && !Array.isArray(root.resources))
|
|
122
|
+
return false;
|
|
123
|
+
if ("format_version" in root ||
|
|
124
|
+
"planned_values" in root ||
|
|
125
|
+
"resource_changes" in root)
|
|
126
|
+
return false;
|
|
127
|
+
if (typeof root.$schema === "string" &&
|
|
128
|
+
root.$schema.includes("deploymentTemplate"))
|
|
129
|
+
return true;
|
|
130
|
+
if (typeof root.contentVersion !== "string" || !Array.isArray(root.resources))
|
|
131
|
+
return false;
|
|
132
|
+
return root.resources.some((item) => String(object(item).type ?? "").startsWith("Microsoft."));
|
|
133
|
+
}
|
|
92
134
|
export function looksLikePulumiYaml(content) {
|
|
93
135
|
return (/^\s*resources\s*:/m.test(content) &&
|
|
94
136
|
/^\s+type\s*:\s*[A-Za-z0-9_.-]+:\S+/m.test(content));
|
|
@@ -114,6 +156,30 @@ export function parseIac(content, requested = "auto") {
|
|
|
114
156
|
(requested === "auto" && looksLikeHelm(content))) {
|
|
115
157
|
return parseHelm(content);
|
|
116
158
|
}
|
|
159
|
+
const pipeline = requested === "gitlab" ||
|
|
160
|
+
requested === "azure-pipelines" ||
|
|
161
|
+
requested === "cloudbuild"
|
|
162
|
+
? requested
|
|
163
|
+
: requested === "auto"
|
|
164
|
+
? looksLikePipeline(content)
|
|
165
|
+
: undefined;
|
|
166
|
+
if (pipeline) {
|
|
167
|
+
if (!pipelineMatches(content, pipeline))
|
|
168
|
+
throw new ScanInputError(pipelineShapeError(pipeline));
|
|
169
|
+
return parsePipeline(content, pipeline);
|
|
170
|
+
}
|
|
171
|
+
if (requested === "tfvars")
|
|
172
|
+
return parseTfvars(content);
|
|
173
|
+
if (requested === "bicep" || (requested === "auto" && looksLikeBicep(content))) {
|
|
174
|
+
if (!looksLikeBicep(content))
|
|
175
|
+
throw new ScanInputError("A Bicep file requires a Microsoft resource declaration.");
|
|
176
|
+
return parseAzure(content, "bicep");
|
|
177
|
+
}
|
|
178
|
+
if (requested === "arm" || (requested === "auto" && looksLikeArm(content))) {
|
|
179
|
+
if (!looksLikeArm(content))
|
|
180
|
+
throw new ScanInputError("An ARM template requires a deploymentTemplate schema or contentVersion with Microsoft resources.");
|
|
181
|
+
return parseAzure(content, "arm");
|
|
182
|
+
}
|
|
117
183
|
if (requested !== "terraform" &&
|
|
118
184
|
requested !== "compose" &&
|
|
119
185
|
looksLikePulumiProgram(content) &&
|
|
@@ -123,8 +189,7 @@ export function parseIac(content, requested = "auto") {
|
|
|
123
189
|
const warnings = [];
|
|
124
190
|
const resources = [];
|
|
125
191
|
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));
|
|
192
|
+
(requested === "auto" && looksLikeTerraformHcl(content));
|
|
128
193
|
if (isHcl) {
|
|
129
194
|
let data;
|
|
130
195
|
try {
|
|
@@ -157,14 +222,49 @@ export function parseIac(content, requested = "auto") {
|
|
|
157
222
|
});
|
|
158
223
|
}
|
|
159
224
|
}
|
|
225
|
+
for (const [name, blocks] of Object.entries(object(object(data).module))) {
|
|
226
|
+
const declaration = new RegExp(`\\bmodule\\s+"${escapeRegex(name)}"`).exec(searchable);
|
|
227
|
+
const offset = declaration?.index ?? 0;
|
|
228
|
+
const before = content.slice(0, offset);
|
|
229
|
+
resources.push({
|
|
230
|
+
id: `module.${name}`,
|
|
231
|
+
type: "terraform:module",
|
|
232
|
+
value: object(array(blocks)[0]),
|
|
233
|
+
locate: (path = []) => ({
|
|
234
|
+
line: before.split("\n").length,
|
|
235
|
+
column: offset - before.lastIndexOf("\n"),
|
|
236
|
+
path: ["module", name, ...path].join("."),
|
|
237
|
+
}),
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
for (const [name, blocks] of Object.entries(object(object(data).variable))) {
|
|
241
|
+
const body = object(array(blocks)[0]);
|
|
242
|
+
if (!("default" in body))
|
|
243
|
+
continue;
|
|
244
|
+
const declaration = new RegExp(`\\bvariable\\s+"${escapeRegex(name)}"`).exec(searchable);
|
|
245
|
+
const offset = declaration?.index ?? 0;
|
|
246
|
+
const before = content.slice(0, offset);
|
|
247
|
+
resources.push({
|
|
248
|
+
id: `variable.${name}`,
|
|
249
|
+
type: "terraform:variable",
|
|
250
|
+
value: { name, default: body.default },
|
|
251
|
+
locate: (path = []) => ({
|
|
252
|
+
line: before.split("\n").length,
|
|
253
|
+
column: offset - before.lastIndexOf("\n"),
|
|
254
|
+
path: ["variable", name, ...path].join("."),
|
|
255
|
+
}),
|
|
256
|
+
});
|
|
257
|
+
}
|
|
160
258
|
if (object(data).module)
|
|
161
|
-
warnings.push("External
|
|
259
|
+
warnings.push("External modules are not expanded. The module body was not loaded, and the module source is not fetched or evaluated.");
|
|
162
260
|
if (JSON.stringify(data).includes("${"))
|
|
163
261
|
warnings.push("Terraform expressions are not evaluated. Findings use literal values and declared settings.");
|
|
164
|
-
if (!resources.
|
|
165
|
-
warnings.push("No resource declarations found.
|
|
262
|
+
if (!resources.some((item) => item.type !== "terraform:module" && item.type !== "terraform:variable"))
|
|
263
|
+
warnings.push("No resource declarations found. Data sources and outputs are not scanned as resources.");
|
|
166
264
|
return { format: "terraform", resources, warnings };
|
|
167
265
|
}
|
|
266
|
+
if (requested === "auto" && looksLikeTfvars(content))
|
|
267
|
+
return parseTfvars(content);
|
|
168
268
|
const lines = new LineCounter();
|
|
169
269
|
const tags = [
|
|
170
270
|
"Ref",
|
|
@@ -330,7 +430,7 @@ export function parseIac(content, requested = "auto") {
|
|
|
330
430
|
}
|
|
331
431
|
}
|
|
332
432
|
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
|
|
433
|
+
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
434
|
if (!resources.length)
|
|
335
435
|
warnings.push("No resources found in the submitted document.");
|
|
336
436
|
if (/(?:!(?:Ref|Sub|GetAtt|If)\b|"(?:Ref|Fn::\w+)"\s*:|\$\{)/.test(content))
|
|
@@ -517,9 +617,304 @@ function sourceIsSecret(source) {
|
|
|
517
617
|
return true;
|
|
518
618
|
return base === "credentials";
|
|
519
619
|
}
|
|
620
|
+
function secretVariableName(name) {
|
|
621
|
+
const normalized = name.replace(/-/g, "_");
|
|
622
|
+
return (/(?:^|_)(?:password|passwd|secret|token)$/i.test(normalized) ||
|
|
623
|
+
/(?:^|_)(?:api_?key|auth_token|access_key|private_key)$/i.test(normalized));
|
|
624
|
+
}
|
|
625
|
+
function isShellReference(value) {
|
|
626
|
+
return (/^\$[A-Za-z_][A-Za-z0-9_]*$/.test(value) ||
|
|
627
|
+
/^\$\{[A-Za-z_][A-Za-z0-9_]*\}$/.test(value));
|
|
628
|
+
}
|
|
629
|
+
function parseImageAssignments(kind, rest) {
|
|
630
|
+
const trimmed = rest.trim();
|
|
631
|
+
if (!trimmed)
|
|
632
|
+
return [];
|
|
633
|
+
if (kind === "ARG" && /^[A-Za-z_][A-Za-z0-9_]*$/.test(trimmed))
|
|
634
|
+
return [{ name: trimmed, value: "" }];
|
|
635
|
+
if (!trimmed.includes("=")) {
|
|
636
|
+
const spaced = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)\s+([\s\S]*)$/);
|
|
637
|
+
if (!spaced)
|
|
638
|
+
return [];
|
|
639
|
+
return [{ name: spaced[1], value: unquoteAssignment(spaced[2].trim()) }];
|
|
640
|
+
}
|
|
641
|
+
const pairs = [];
|
|
642
|
+
const pattern = /([A-Za-z_][A-Za-z0-9_]*)=(?:"([^"]*)"|'([^']*)'|([^\s]*))/g;
|
|
643
|
+
for (const match of trimmed.matchAll(pattern)) {
|
|
644
|
+
pairs.push({
|
|
645
|
+
name: match[1],
|
|
646
|
+
value: match[2] ?? match[3] ?? match[4] ?? "",
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
return pairs;
|
|
650
|
+
}
|
|
651
|
+
function unquoteAssignment(value) {
|
|
652
|
+
if ((value.startsWith('"') && value.endsWith('"') && value.length >= 2) ||
|
|
653
|
+
(value.startsWith("'") && value.endsWith("'") && value.length >= 2))
|
|
654
|
+
return value.slice(1, -1);
|
|
655
|
+
return value;
|
|
656
|
+
}
|
|
657
|
+
function workflowEnvIsLiteral(value) {
|
|
658
|
+
const trimmed = value.trim();
|
|
659
|
+
if (!trimmed)
|
|
660
|
+
return false;
|
|
661
|
+
if (/^\$\{\{\s*secrets\.[^}]+\}\}$/i.test(trimmed))
|
|
662
|
+
return false;
|
|
663
|
+
if (/^\$\{\{\s*github\.token\s*\}\}$/i.test(trimmed))
|
|
664
|
+
return false;
|
|
665
|
+
return true;
|
|
666
|
+
}
|
|
520
667
|
function lineLocation(line, path) {
|
|
521
668
|
return { line, column: 1, path };
|
|
522
669
|
}
|
|
670
|
+
function stolenPipelineInput(content) {
|
|
671
|
+
return (looksLikeGithubWorkflow(content) ||
|
|
672
|
+
looksLikeAnsible(content) ||
|
|
673
|
+
looksLikeHelm(content) ||
|
|
674
|
+
(topLevelKey(content, "apiVersion") && topLevelKey(content, "kind")));
|
|
675
|
+
}
|
|
676
|
+
function azurePipelineShape(content) {
|
|
677
|
+
const poolOrTrigger = topLevelKey(content, "pool") || topLevelKey(content, "trigger");
|
|
678
|
+
const stepsOrStages = topLevelKey(content, "steps") || topLevelKey(content, "stages");
|
|
679
|
+
return poolOrTrigger && stepsOrStages;
|
|
680
|
+
}
|
|
681
|
+
function gitlabShape(content) {
|
|
682
|
+
if (!/\bimage\s*:/.test(content))
|
|
683
|
+
return false;
|
|
684
|
+
if (topLevelKey(content, "stages"))
|
|
685
|
+
return true;
|
|
686
|
+
return /^\s*script\s*:/m.test(content);
|
|
687
|
+
}
|
|
688
|
+
function cloudBuildShape(content) {
|
|
689
|
+
if (!topLevelKey(content, "steps"))
|
|
690
|
+
return false;
|
|
691
|
+
if (topLevelKey(content, "on") && topLevelKey(content, "jobs"))
|
|
692
|
+
return false;
|
|
693
|
+
return /^\s*-\s*name\s*:/m.test(content);
|
|
694
|
+
}
|
|
695
|
+
function looksLikePipeline(content) {
|
|
696
|
+
if (stolenPipelineInput(content))
|
|
697
|
+
return undefined;
|
|
698
|
+
const visible = maskIaCComments(content);
|
|
699
|
+
if (azurePipelineShape(visible))
|
|
700
|
+
return "azure-pipelines";
|
|
701
|
+
if (gitlabShape(visible))
|
|
702
|
+
return "gitlab";
|
|
703
|
+
if (cloudBuildShape(visible))
|
|
704
|
+
return "cloudbuild";
|
|
705
|
+
return undefined;
|
|
706
|
+
}
|
|
707
|
+
function pipelineMatches(content, format) {
|
|
708
|
+
if (stolenPipelineInput(content))
|
|
709
|
+
return false;
|
|
710
|
+
const visible = maskIaCComments(content);
|
|
711
|
+
if (format === "azure-pipelines")
|
|
712
|
+
return azurePipelineShape(visible);
|
|
713
|
+
if (format === "gitlab")
|
|
714
|
+
return gitlabShape(visible);
|
|
715
|
+
return cloudBuildShape(visible);
|
|
716
|
+
}
|
|
717
|
+
function pipelineShapeError(format) {
|
|
718
|
+
if (format === "gitlab")
|
|
719
|
+
return "GitLab CI requires top-level stages or a job with script, and an image.";
|
|
720
|
+
if (format === "azure-pipelines")
|
|
721
|
+
return "Azure Pipelines requires a top-level pool or trigger, plus steps or stages.";
|
|
722
|
+
return "Cloud Build requires top-level steps whose items have a name.";
|
|
723
|
+
}
|
|
724
|
+
const pipelineWarning = "The pipeline is not executed. Only literal text outside comments is checked.";
|
|
725
|
+
function scriptText(value) {
|
|
726
|
+
if (typeof value === "string")
|
|
727
|
+
return value;
|
|
728
|
+
if (Array.isArray(value) && value.every((item) => typeof item === "string"))
|
|
729
|
+
return value.join("\n");
|
|
730
|
+
return undefined;
|
|
731
|
+
}
|
|
732
|
+
function pipelineScriptKey(key) {
|
|
733
|
+
return (key === "script" ||
|
|
734
|
+
key === "before_script" ||
|
|
735
|
+
key === "after_script" ||
|
|
736
|
+
key === "bash" ||
|
|
737
|
+
key === "pwsh" ||
|
|
738
|
+
key === "powershell");
|
|
739
|
+
}
|
|
740
|
+
function pipelinePrintsSecret(script) {
|
|
741
|
+
return script.split(/\n/).some((line) => {
|
|
742
|
+
const code = line.replace(/(^|\s)#.*$/, "").trim();
|
|
743
|
+
if (!/\b(?:echo|printf|print)\b/i.test(code))
|
|
744
|
+
return false;
|
|
745
|
+
return /CI_JOB_TOKEN|secrets\.|\$\(secret|(?:^|[^A-Za-z0-9_])(?:password|passwd|api_key|apikey|auth_token|access_key|private_key|token)(?:[^A-Za-z0-9_]|$)/i.test(code);
|
|
746
|
+
});
|
|
747
|
+
}
|
|
748
|
+
function collectPipelineHits(value, path, format, hits) {
|
|
749
|
+
if (Array.isArray(value)) {
|
|
750
|
+
value.forEach((item, index) => collectPipelineHits(item, [...path, index], format, hits));
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
if (value === null || typeof value !== "object")
|
|
754
|
+
return;
|
|
755
|
+
for (const [key, child] of Object.entries(object(value))) {
|
|
756
|
+
const childPath = [...path, key];
|
|
757
|
+
if ((key === "image" || key === "container") && typeof child === "string")
|
|
758
|
+
hits.push({ kind: "image", path: childPath, image: child });
|
|
759
|
+
else if (key === "image" &&
|
|
760
|
+
typeof object(child).name === "string")
|
|
761
|
+
hits.push({
|
|
762
|
+
kind: "image",
|
|
763
|
+
path: [...childPath, "name"],
|
|
764
|
+
image: String(object(child).name),
|
|
765
|
+
});
|
|
766
|
+
if (key === "privileged" && child === true)
|
|
767
|
+
hits.push({ kind: "privileged", path: childPath });
|
|
768
|
+
const script = pipelineScriptKey(key) ? scriptText(child) : undefined;
|
|
769
|
+
if (script)
|
|
770
|
+
hits.push({ kind: "script", path: childPath, script });
|
|
771
|
+
if (format === "cloudbuild" && key === "steps" && Array.isArray(child)) {
|
|
772
|
+
child.forEach((step, index) => {
|
|
773
|
+
const name = object(step).name;
|
|
774
|
+
if (typeof name === "string")
|
|
775
|
+
hits.push({
|
|
776
|
+
kind: "image",
|
|
777
|
+
path: [...childPath, index, "name"],
|
|
778
|
+
image: name,
|
|
779
|
+
});
|
|
780
|
+
const args = scriptText(object(step).args);
|
|
781
|
+
if (args)
|
|
782
|
+
hits.push({
|
|
783
|
+
kind: "script",
|
|
784
|
+
path: [...childPath, index, "args"],
|
|
785
|
+
script: args,
|
|
786
|
+
});
|
|
787
|
+
});
|
|
788
|
+
}
|
|
789
|
+
collectPipelineHits(child, childPath, format, hits);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
function parsePipeline(content, format) {
|
|
793
|
+
const lines = new LineCounter();
|
|
794
|
+
let documents;
|
|
795
|
+
try {
|
|
796
|
+
documents = parseAllDocuments(content, {
|
|
797
|
+
lineCounter: lines,
|
|
798
|
+
prettyErrors: false,
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
catch {
|
|
802
|
+
throw new ScanInputError(pipelineShapeError(format));
|
|
803
|
+
}
|
|
804
|
+
const resources = [];
|
|
805
|
+
let found = false;
|
|
806
|
+
for (const [documentIndex, doc] of documents.entries()) {
|
|
807
|
+
if (doc.errors.length)
|
|
808
|
+
throw new ScanInputError(pipelineShapeError(format));
|
|
809
|
+
let data;
|
|
810
|
+
try {
|
|
811
|
+
data = doc.toJS({ maxAliasCount: 0 });
|
|
812
|
+
}
|
|
813
|
+
catch {
|
|
814
|
+
throw new ScanInputError("YAML aliases are not supported. Expand anchors before scanning.");
|
|
815
|
+
}
|
|
816
|
+
if (data == null)
|
|
817
|
+
continue;
|
|
818
|
+
checkShape(data);
|
|
819
|
+
found = true;
|
|
820
|
+
const hits = [];
|
|
821
|
+
collectPipelineHits(data, [], format, hits);
|
|
822
|
+
const position = (path) => {
|
|
823
|
+
let node = doc.getIn(path, true);
|
|
824
|
+
if (!node?.range)
|
|
825
|
+
node = doc.getIn([], true);
|
|
826
|
+
const at = lines.linePos(node?.range?.[0] ?? doc.range?.[0] ?? 0);
|
|
827
|
+
return {
|
|
828
|
+
line: at.line,
|
|
829
|
+
column: at.col,
|
|
830
|
+
path: [documentIndex, ...path].join("."),
|
|
831
|
+
};
|
|
832
|
+
};
|
|
833
|
+
hits.forEach((hit, index) => {
|
|
834
|
+
if (hit.kind === "image" && hit.image)
|
|
835
|
+
resources.push({
|
|
836
|
+
id: `pipeline.image.${index + 1}`,
|
|
837
|
+
type: "pipeline:image",
|
|
838
|
+
value: { unpinned: imageIsUnpinned(hit.image.trim(), new Set()) },
|
|
839
|
+
locate: () => position(hit.path),
|
|
840
|
+
});
|
|
841
|
+
else if (hit.kind === "script" && hit.script)
|
|
842
|
+
resources.push({
|
|
843
|
+
id: `pipeline.script.${index + 1}`,
|
|
844
|
+
type: "pipeline:script",
|
|
845
|
+
value: { printsSecret: pipelinePrintsSecret(hit.script) },
|
|
846
|
+
locate: () => position(hit.path),
|
|
847
|
+
});
|
|
848
|
+
else if (hit.kind === "privileged")
|
|
849
|
+
resources.push({
|
|
850
|
+
id: `pipeline.privileged.${index + 1}`,
|
|
851
|
+
type: "pipeline:privileged",
|
|
852
|
+
value: { present: true },
|
|
853
|
+
locate: () => position(hit.path),
|
|
854
|
+
});
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
if (!found)
|
|
858
|
+
throw new ScanInputError(pipelineShapeError(format));
|
|
859
|
+
return { format, resources, warnings: [pipelineWarning] };
|
|
860
|
+
}
|
|
861
|
+
function looksLikeTfvars(content) {
|
|
862
|
+
const visible = maskIaCComments(content);
|
|
863
|
+
if (/^\s*(?:resource|module|provider)\s+["']/m.test(visible))
|
|
864
|
+
return false;
|
|
865
|
+
if (/^\s*(?:terraform|provider|module)\s*\{/m.test(visible))
|
|
866
|
+
return false;
|
|
867
|
+
return /^\s*[A-Za-z_][A-Za-z0-9_]*\s*=\s*\S/m.test(visible);
|
|
868
|
+
}
|
|
869
|
+
function parseTfvars(content) {
|
|
870
|
+
let data;
|
|
871
|
+
try {
|
|
872
|
+
const [parsed, error] = hcl.parseToObject(content);
|
|
873
|
+
if (error || !parsed)
|
|
874
|
+
throw new Error("parse");
|
|
875
|
+
data = parsed;
|
|
876
|
+
}
|
|
877
|
+
catch {
|
|
878
|
+
throw new ScanInputError("Invalid tfvars. Check assignment syntax.");
|
|
879
|
+
}
|
|
880
|
+
checkShape(data);
|
|
881
|
+
const searchable = maskIaCComments(content);
|
|
882
|
+
const resources = [];
|
|
883
|
+
const skipped = new Set([
|
|
884
|
+
"resource",
|
|
885
|
+
"module",
|
|
886
|
+
"terraform",
|
|
887
|
+
"provider",
|
|
888
|
+
"variable",
|
|
889
|
+
"data",
|
|
890
|
+
"output",
|
|
891
|
+
"locals",
|
|
892
|
+
]);
|
|
893
|
+
for (const [name, value] of Object.entries(object(data))) {
|
|
894
|
+
if (skipped.has(name))
|
|
895
|
+
continue;
|
|
896
|
+
const declaration = new RegExp(`(^|\\n)\\s*${escapeRegex(name)}\\s*=`).exec(searchable);
|
|
897
|
+
const offset = declaration?.index ?? 0;
|
|
898
|
+
const before = content.slice(0, offset);
|
|
899
|
+
resources.push({
|
|
900
|
+
id: `tfvars.${name}`,
|
|
901
|
+
type: "tfvars:assignment",
|
|
902
|
+
value: { name, value },
|
|
903
|
+
locate: (path = []) => ({
|
|
904
|
+
line: before.split("\n").length,
|
|
905
|
+
column: 1,
|
|
906
|
+
path: ["tfvars", name, ...path].join("."),
|
|
907
|
+
}),
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
return {
|
|
911
|
+
format: "tfvars",
|
|
912
|
+
resources,
|
|
913
|
+
warnings: [
|
|
914
|
+
"Terraform variable references are not evaluated. Findings use literal values.",
|
|
915
|
+
],
|
|
916
|
+
};
|
|
917
|
+
}
|
|
523
918
|
function parseDockerfile(content) {
|
|
524
919
|
const instructions = logicalDockerfileLines(content);
|
|
525
920
|
const froms = instructions.flatMap((instruction) => {
|
|
@@ -576,6 +971,22 @@ function parseDockerfile(content) {
|
|
|
576
971
|
locate: () => lineLocation(instruction.line, "copy"),
|
|
577
972
|
});
|
|
578
973
|
});
|
|
974
|
+
instructions.forEach((instruction, index) => {
|
|
975
|
+
const header = instruction.text.match(/^(ENV|ARG)\b\s*([\s\S]*)$/i);
|
|
976
|
+
if (!header)
|
|
977
|
+
return;
|
|
978
|
+
const kind = header[1].toUpperCase() === "ARG" ? "ARG" : "ENV";
|
|
979
|
+
const assignments = parseImageAssignments(kind, header[2] ?? "");
|
|
980
|
+
const literal = assignments.some((item) => secretVariableName(item.name) &&
|
|
981
|
+
item.value.trim() !== "" &&
|
|
982
|
+
!isShellReference(item.value.trim()));
|
|
983
|
+
resources.push({
|
|
984
|
+
id: `dockerfile.env.${index + 1}`,
|
|
985
|
+
type: "dockerfile:env",
|
|
986
|
+
value: { literal },
|
|
987
|
+
locate: () => lineLocation(instruction.line, "env"),
|
|
988
|
+
});
|
|
989
|
+
});
|
|
579
990
|
return {
|
|
580
991
|
format: "dockerfile",
|
|
581
992
|
resources,
|
|
@@ -678,7 +1089,27 @@ function parseGithubWorkflow(content) {
|
|
|
678
1089
|
value: { broad },
|
|
679
1090
|
locate: () => position(root.permissions != null ? ["permissions"] : ["on"]),
|
|
680
1091
|
});
|
|
1092
|
+
const addEnv = (envValue, path) => {
|
|
1093
|
+
for (const [key, raw] of Object.entries(object(envValue))) {
|
|
1094
|
+
if (typeof raw !== "string" && raw != null)
|
|
1095
|
+
continue;
|
|
1096
|
+
if (!secretVariableName(key))
|
|
1097
|
+
continue;
|
|
1098
|
+
const value = typeof raw === "string" ? raw : "";
|
|
1099
|
+
resources.push({
|
|
1100
|
+
id: `env.${[...path, key].join(".")}`,
|
|
1101
|
+
type: "github:env",
|
|
1102
|
+
value: { literal: workflowEnvIsLiteral(value) },
|
|
1103
|
+
locate: () => position([...path, key]),
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
};
|
|
1107
|
+
addEnv(root.env, ["env"]);
|
|
681
1108
|
for (const [jobName, job] of Object.entries(object(root.jobs))) {
|
|
1109
|
+
addEnv(object(job).env, ["jobs", jobName, "env"]);
|
|
1110
|
+
array(object(job).steps).forEach((step, index) => {
|
|
1111
|
+
addEnv(object(step).env, ["jobs", jobName, "steps", index, "env"]);
|
|
1112
|
+
});
|
|
682
1113
|
array(object(job).steps).forEach((step, index) => {
|
|
683
1114
|
const uses = object(step).uses;
|
|
684
1115
|
if (typeof uses !== "string")
|
|
@@ -836,3 +1267,105 @@ function parseAnsible(content) {
|
|
|
836
1267
|
warnings: [ansibleFactsWarning],
|
|
837
1268
|
};
|
|
838
1269
|
}
|
|
1270
|
+
function maskIaCComments(content) {
|
|
1271
|
+
const withoutBlock = content.replace(/\/\*[\s\S]*?\*\//g, (match) => match.replace(/[^\n]/g, " "));
|
|
1272
|
+
return withoutBlock
|
|
1273
|
+
.split("\n")
|
|
1274
|
+
.map((line) => {
|
|
1275
|
+
let out = "";
|
|
1276
|
+
let quote;
|
|
1277
|
+
for (let index = 0; index < line.length; index++) {
|
|
1278
|
+
const char = line[index];
|
|
1279
|
+
const next = line[index + 1];
|
|
1280
|
+
if (quote) {
|
|
1281
|
+
out += char;
|
|
1282
|
+
if (char === "\\" && quote === '"') {
|
|
1283
|
+
if (next)
|
|
1284
|
+
out += next;
|
|
1285
|
+
index += 1;
|
|
1286
|
+
continue;
|
|
1287
|
+
}
|
|
1288
|
+
if (char === quote)
|
|
1289
|
+
quote = undefined;
|
|
1290
|
+
continue;
|
|
1291
|
+
}
|
|
1292
|
+
if (char === '"' || char === "'") {
|
|
1293
|
+
quote = char;
|
|
1294
|
+
out += char;
|
|
1295
|
+
continue;
|
|
1296
|
+
}
|
|
1297
|
+
if ((char === "/" && next === "/") ||
|
|
1298
|
+
char === "#") {
|
|
1299
|
+
return out + " ".repeat(line.length - out.length);
|
|
1300
|
+
}
|
|
1301
|
+
out += char;
|
|
1302
|
+
}
|
|
1303
|
+
return out;
|
|
1304
|
+
})
|
|
1305
|
+
.join("\n");
|
|
1306
|
+
}
|
|
1307
|
+
function addressValueIsOpen(after) {
|
|
1308
|
+
return /(?:^|[\s\[,])(?:"\*"|"0\.0\.0\.0\/0"|'\*'|'0\.0\.0\.0\/0'|\*|0\.0\.0\.0\/0)(?=$|[\s,\]}"'])/.test(after);
|
|
1309
|
+
}
|
|
1310
|
+
function nearestScopeIsEgress(content, index) {
|
|
1311
|
+
const windowStart = Math.max(0, index - 800);
|
|
1312
|
+
const windowEnd = Math.min(content.length, index + 400);
|
|
1313
|
+
const slice = content.slice(windowStart, windowEnd);
|
|
1314
|
+
const relative = index - windowStart;
|
|
1315
|
+
let nearest;
|
|
1316
|
+
for (const match of slice.matchAll(/\b(Inbound|Outbound|ingress|egress)\b/gi)) {
|
|
1317
|
+
const at = match.index ?? 0;
|
|
1318
|
+
const distance = Math.abs(at - relative);
|
|
1319
|
+
const egress = /^(?:outbound|egress)$/i.test(match[1] ?? "");
|
|
1320
|
+
if (!nearest || distance < nearest.distance)
|
|
1321
|
+
nearest = { distance, egress };
|
|
1322
|
+
}
|
|
1323
|
+
return nearest?.egress ?? false;
|
|
1324
|
+
}
|
|
1325
|
+
function firstOpenAddress(content) {
|
|
1326
|
+
const pattern = /(source|destination)AddressPrefix(es)?["']?\s*[:=]/gi;
|
|
1327
|
+
for (const match of content.matchAll(pattern)) {
|
|
1328
|
+
const kind = (match[1] ?? "").toLowerCase();
|
|
1329
|
+
const index = match.index ?? 0;
|
|
1330
|
+
const after = content.slice(index + match[0].length, index + match[0].length + 240);
|
|
1331
|
+
if (!addressValueIsOpen(after))
|
|
1332
|
+
continue;
|
|
1333
|
+
if (kind === "destination" && nearestScopeIsEgress(content, index))
|
|
1334
|
+
continue;
|
|
1335
|
+
return { present: true, line: lineAt(content, index) };
|
|
1336
|
+
}
|
|
1337
|
+
return { present: false, line: 1 };
|
|
1338
|
+
}
|
|
1339
|
+
function enclosingAzureType(content, index) {
|
|
1340
|
+
const before = content.slice(Math.max(0, index - 4000), index);
|
|
1341
|
+
const declarations = [
|
|
1342
|
+
...before.matchAll(/resource\s+[A-Za-z_][\w]*\s+['"](Microsoft\.[^'"]+)['"]|(?:["']type["']|type)\s*:\s*["'](Microsoft\.[^"']+)["']/g),
|
|
1343
|
+
];
|
|
1344
|
+
const last = declarations.at(-1);
|
|
1345
|
+
return last?.[1] ?? last?.[2] ?? "";
|
|
1346
|
+
}
|
|
1347
|
+
function firstPublicNetwork(content) {
|
|
1348
|
+
const pattern = /publicNetworkAccess["']?\s*[:=]\s*(["']?)(Enabled|true|Disabled|false)\1(?![\w.-])/gi;
|
|
1349
|
+
for (const match of content.matchAll(pattern)) {
|
|
1350
|
+
if (!/^(?:enabled|true)$/i.test(match[2] ?? ""))
|
|
1351
|
+
continue;
|
|
1352
|
+
const index = match.index ?? 0;
|
|
1353
|
+
const type = enclosingAzureType(content, index);
|
|
1354
|
+
if (!/(?:database|storage|server)/i.test(type))
|
|
1355
|
+
continue;
|
|
1356
|
+
return { present: true, line: lineAt(content, index) };
|
|
1357
|
+
}
|
|
1358
|
+
return { present: false, line: 1 };
|
|
1359
|
+
}
|
|
1360
|
+
const azureExpressionWarning = "Bicep modules and ARM expressions are not evaluated.";
|
|
1361
|
+
function parseAzure(content, format) {
|
|
1362
|
+
const visible = maskIaCComments(content);
|
|
1363
|
+
return {
|
|
1364
|
+
format,
|
|
1365
|
+
resources: [
|
|
1366
|
+
literalResource("azure.exposure", "azure:exposure", firstOpenAddress(visible), "sourceAddressPrefix"),
|
|
1367
|
+
literalResource("azure.publicNetwork", "azure:publicNetwork", firstPublicNetwork(visible), "publicNetworkAccess"),
|
|
1368
|
+
],
|
|
1369
|
+
warnings: [azureExpressionWarning],
|
|
1370
|
+
};
|
|
1371
|
+
}
|
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",
|
|
@@ -295,6 +323,34 @@ export const rules = {
|
|
|
295
323
|
detail: "A cidr or source allows 0.0.0.0/0.",
|
|
296
324
|
remediation: "Restrict the cidr or source to approved addresses.",
|
|
297
325
|
},
|
|
326
|
+
PIPE001: {
|
|
327
|
+
category: "security",
|
|
328
|
+
severity: "medium",
|
|
329
|
+
title: "Unpinned container image",
|
|
330
|
+
detail: "An image or step name uses the latest tag, or it has no tag and no digest.",
|
|
331
|
+
remediation: "Pin a release tag or an image digest.",
|
|
332
|
+
},
|
|
333
|
+
PIPE002: {
|
|
334
|
+
category: "security",
|
|
335
|
+
severity: "high",
|
|
336
|
+
title: "Pipeline prints a secret",
|
|
337
|
+
detail: "A script line echoes or prints a secret-like variable.",
|
|
338
|
+
remediation: "Remove the printed secret from the script. Pass secrets through a variable the script does not print.",
|
|
339
|
+
},
|
|
340
|
+
PIPE003: {
|
|
341
|
+
category: "security",
|
|
342
|
+
severity: "critical",
|
|
343
|
+
title: "Elevated container privileges",
|
|
344
|
+
detail: "The pipeline sets privileged to true.",
|
|
345
|
+
remediation: "Set privileged to false. Isolate workloads that genuinely require elevated privileges.",
|
|
346
|
+
},
|
|
347
|
+
TF001: {
|
|
348
|
+
category: "security",
|
|
349
|
+
severity: "high",
|
|
350
|
+
title: "Secret assigned in a Terraform variable",
|
|
351
|
+
detail: "A Terraform variable default or tfvars assignment gives a non-empty literal to a secret-like name.",
|
|
352
|
+
remediation: "Pass the value from a secret store or leave the default empty. Do not commit a literal secret.",
|
|
353
|
+
},
|
|
298
354
|
};
|
|
299
355
|
export function resourceFindings(resources, format) {
|
|
300
356
|
const findings = [];
|
|
@@ -326,6 +382,28 @@ export function resourceFindings(resources, format) {
|
|
|
326
382
|
checkAnsible(resource, check);
|
|
327
383
|
continue;
|
|
328
384
|
}
|
|
385
|
+
if (format === "bicep" || format === "arm") {
|
|
386
|
+
checkAzure(resource, check);
|
|
387
|
+
continue;
|
|
388
|
+
}
|
|
389
|
+
if (format === "gitlab" ||
|
|
390
|
+
format === "azure-pipelines" ||
|
|
391
|
+
format === "cloudbuild") {
|
|
392
|
+
checkPipeline(resource, check);
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
if (resource.type === "terraform:module") {
|
|
396
|
+
checkTerraformLiterals(resource.value, "module", check, []);
|
|
397
|
+
continue;
|
|
398
|
+
}
|
|
399
|
+
if (resource.type === "terraform:variable") {
|
|
400
|
+
checkTerraformLiterals({ [String(resource.value.name ?? "default")]: resource.value.default }, "assignment", check, ["default"]);
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
if (format === "tfvars" || resource.type === "tfvars:assignment") {
|
|
404
|
+
checkTerraformLiterals({ [String(resource.value.name ?? "value")]: resource.value.value }, "assignment", check, []);
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
329
407
|
walk(resource.value, (key, value, path, parent) => {
|
|
330
408
|
if (typeof value !== "string")
|
|
331
409
|
return;
|
|
@@ -355,6 +433,8 @@ function checkDockerfile(resource, check) {
|
|
|
355
433
|
check("DF003", resource.value.piped === true, ["run"]);
|
|
356
434
|
else if (resource.type === "dockerfile:copy")
|
|
357
435
|
check("DF004", resource.value.secret === true, ["copy"]);
|
|
436
|
+
else if (resource.type === "dockerfile:env")
|
|
437
|
+
check("DF005", resource.value.literal === true, ["env"]);
|
|
358
438
|
}
|
|
359
439
|
function checkGithub(resource, check) {
|
|
360
440
|
if (resource.type === "github:trigger")
|
|
@@ -366,6 +446,8 @@ function checkGithub(resource, check) {
|
|
|
366
446
|
check("GH003", resource.value.floating === true, ["uses"]);
|
|
367
447
|
else if (resource.type === "github:run")
|
|
368
448
|
check("GH004", resource.value.printsSecret === true, ["run"]);
|
|
449
|
+
else if (resource.type === "github:env")
|
|
450
|
+
check("GH005", resource.value.literal === true, ["env"]);
|
|
369
451
|
}
|
|
370
452
|
function checkHelm(resource, check) {
|
|
371
453
|
if (resource.type === "helm:privileged")
|
|
@@ -383,6 +465,56 @@ function checkAnsible(resource, check) {
|
|
|
383
465
|
else if (resource.type === "ansible:cidr")
|
|
384
466
|
check("ANS002", resource.value.present === true, ["cidr"]);
|
|
385
467
|
}
|
|
468
|
+
function checkPipeline(resource, check) {
|
|
469
|
+
if (resource.type === "pipeline:image")
|
|
470
|
+
check("PIPE001", resource.value.unpinned === true, ["image"]);
|
|
471
|
+
else if (resource.type === "pipeline:script")
|
|
472
|
+
check("PIPE002", resource.value.printsSecret === true, ["script"]);
|
|
473
|
+
else if (resource.type === "pipeline:privileged")
|
|
474
|
+
check("PIPE003", resource.value.present === true, ["privileged"]);
|
|
475
|
+
}
|
|
476
|
+
function terraformReference(value) {
|
|
477
|
+
const trimmed = value.trim();
|
|
478
|
+
return trimmed.includes("${") || /^var\./.test(trimmed);
|
|
479
|
+
}
|
|
480
|
+
function secretAssignmentName(name) {
|
|
481
|
+
const normalized = name.replace(/-/g, "_");
|
|
482
|
+
return (/(?:^|_)(?:password|passwd|secret|token)$/i.test(normalized) ||
|
|
483
|
+
/(?:^|_)(?:api_?key|auth_token|access_key|private_key)$/i.test(normalized));
|
|
484
|
+
}
|
|
485
|
+
function moduleCidrExempt(name) {
|
|
486
|
+
const normalized = name.trim().toLowerCase().replace(/[\s-]+/g, "_");
|
|
487
|
+
return (normalized === "egress" ||
|
|
488
|
+
normalized === "destination" ||
|
|
489
|
+
normalized === "ipv6_egress");
|
|
490
|
+
}
|
|
491
|
+
function checkTerraformLiterals(value, mode, check, path, name = "") {
|
|
492
|
+
if (typeof value === "string" || typeof value === "boolean") {
|
|
493
|
+
const open = value === "0.0.0.0/0" || value === "::/0";
|
|
494
|
+
const exempt = mode === "module" ? moduleCidrExempt(name) : name.toLowerCase() === "egress";
|
|
495
|
+
if (typeof value === "string")
|
|
496
|
+
check("NET001", open && name !== "" && !exempt, path);
|
|
497
|
+
if (name === "publicly_accessible")
|
|
498
|
+
check("DB001", value === true, path);
|
|
499
|
+
if (mode === "assignment" && typeof value === "string" && secretAssignmentName(name))
|
|
500
|
+
check("TF001", value.trim() !== "" && !terraformReference(value), path);
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
if (Array.isArray(value)) {
|
|
504
|
+
value.forEach((item, index) => checkTerraformLiterals(item, mode, check, [...path, index], name));
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
if (value && typeof value === "object") {
|
|
508
|
+
for (const [key, child] of Object.entries(value))
|
|
509
|
+
checkTerraformLiterals(child, mode, check, [...path, key], key);
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
function checkAzure(resource, check) {
|
|
513
|
+
if (resource.type === "azure:exposure")
|
|
514
|
+
check("AZ001", resource.value.present === true, ["sourceAddressPrefix"]);
|
|
515
|
+
else if (resource.type === "azure:publicNetwork")
|
|
516
|
+
check("AZ002", resource.value.present === true, ["publicNetworkAccess"]);
|
|
517
|
+
}
|
|
386
518
|
function walk(value, visit, path = []) {
|
|
387
519
|
if (Array.isArray(value))
|
|
388
520
|
value.forEach((child, index) => walk(child, visit, [...path, index]));
|
package/dist/server.js
CHANGED
|
@@ -77,12 +77,18 @@ export function createServer(remote = false) {
|
|
|
77
77
|
"github",
|
|
78
78
|
"helm",
|
|
79
79
|
"ansible",
|
|
80
|
+
"bicep",
|
|
81
|
+
"arm",
|
|
82
|
+
"gitlab",
|
|
83
|
+
"azure-pipelines",
|
|
84
|
+
"cloudbuild",
|
|
85
|
+
"tfvars",
|
|
80
86
|
"application",
|
|
81
87
|
"auto",
|
|
82
88
|
])
|
|
83
89
|
.optional()
|
|
84
90
|
.default("auto")
|
|
85
|
-
.describe("Content format: terraform, cloudformation, kubernetes, pulumi, compose, dockerfile, github, helm, ansible, application, auto."),
|
|
91
|
+
.describe("Content format: terraform, cloudformation, kubernetes, pulumi, compose, dockerfile, github, helm, ansible, bicep, arm, gitlab, azure-pipelines, cloudbuild, tfvars, application, auto."),
|
|
86
92
|
},
|
|
87
93
|
}, async ({ content, focus, format, minSeverity, maxFindings }) => {
|
|
88
94
|
const botHints = fetchPublicBotHints();
|
|
@@ -130,7 +136,7 @@ export function createServer(remote = false) {
|
|
|
130
136
|
description: (name === "scan_iac_stub"
|
|
131
137
|
? "Compatibility alias for scan_iac; runs the full scanner. "
|
|
132
138
|
: "") +
|
|
133
|
-
"Parse Terraform HCL/JSON, CloudFormation YAML/JSON, Kubernetes manifests, Pulumi YAML, Pulumi programs, or Docker Compose. Includes Dockerfile, GitHub Actions, Helm templates,
|
|
139
|
+
"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, ARM templates, GitLab CI, Azure Pipelines, Cloud Build, and tfvars. Helm templates, Ansible facts, Bicep modules, ARM expressions, and pipelines are not executed. Terraform module sources are not fetched. Check security, reliability and cost rules with resource locations and remediation. Pulumi programs are scanned as text.",
|
|
134
140
|
annotations: {
|
|
135
141
|
readOnlyHint: true,
|
|
136
142
|
destructiveHint: false,
|
|
@@ -138,7 +144,7 @@ export function createServer(remote = false) {
|
|
|
138
144
|
},
|
|
139
145
|
_meta: { securitySchemes: [{ type: "oauth2", scopes: ["mcp"] }] },
|
|
140
146
|
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,
|
|
147
|
+
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, ARM templates, GitLab CI, Azure Pipelines, Cloud Build, and tfvars."),
|
|
142
148
|
format: z
|
|
143
149
|
.enum([
|
|
144
150
|
"terraform",
|
|
@@ -150,6 +156,12 @@ export function createServer(remote = false) {
|
|
|
150
156
|
"github",
|
|
151
157
|
"helm",
|
|
152
158
|
"ansible",
|
|
159
|
+
"bicep",
|
|
160
|
+
"arm",
|
|
161
|
+
"gitlab",
|
|
162
|
+
"azure-pipelines",
|
|
163
|
+
"cloudbuild",
|
|
164
|
+
"tfvars",
|
|
153
165
|
"auto",
|
|
154
166
|
])
|
|
155
167
|
.default("auto"),
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.6.
|
|
1
|
+
export declare const VERSION = "0.6.12";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.6.
|
|
1
|
+
export const VERSION = "0.6.12";
|