@skaleagents/swarm 0.6.11 → 0.6.13
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 +565 -6
- package/dist/iac/rules.js +124 -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/dist/iac/parse.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type IacFormat = "terraform" | "cloudformation" | "kubernetes" | "pulumi" | "compose" | "dockerfile" | "github" | "helm" | "ansible" | "bicep" | "arm";
|
|
1
|
+
export type IacFormat = "terraform" | "cloudformation" | "kubernetes" | "pulumi" | "compose" | "dockerfile" | "github" | "helm" | "ansible" | "bicep" | "arm" | "gitlab" | "azure-pipelines" | "cloudbuild" | "tfvars" | "serverless" | "iam";
|
|
2
2
|
export type Path = (string | number)[];
|
|
3
3
|
export type Location = {
|
|
4
4
|
line: number;
|
|
@@ -33,3 +33,5 @@ export declare function looksLikeBicep(content: string): boolean;
|
|
|
33
33
|
export declare function looksLikeArm(content: string): boolean;
|
|
34
34
|
export declare function looksLikePulumiYaml(content: string): boolean;
|
|
35
35
|
export declare function parseIac(content: string, requested?: IacFormat | "auto"): ParsedIac;
|
|
36
|
+
export declare function looksLikeServerless(content: string): boolean;
|
|
37
|
+
export declare function looksLikeIamPolicy(content: string): boolean;
|
package/dist/iac/parse.js
CHANGED
|
@@ -30,6 +30,8 @@ export function looksLikeIac(content) {
|
|
|
30
30
|
const value = object(JSON.parse(content));
|
|
31
31
|
if (value.resource || value.Resources || (value.apiVersion && value.kind))
|
|
32
32
|
return true;
|
|
33
|
+
if (looksLikeIamPolicy(content))
|
|
34
|
+
return true;
|
|
33
35
|
}
|
|
34
36
|
catch {
|
|
35
37
|
/* Other format detection still applies to incomplete input. */
|
|
@@ -46,7 +48,11 @@ export function looksLikeIac(content) {
|
|
|
46
48
|
looksLikeHelm(content) ||
|
|
47
49
|
looksLikeAnsible(content) ||
|
|
48
50
|
looksLikeBicep(content) ||
|
|
49
|
-
looksLikeArm(content)
|
|
51
|
+
looksLikeArm(content) ||
|
|
52
|
+
looksLikePipeline(content) !== undefined ||
|
|
53
|
+
looksLikeTfvars(content) ||
|
|
54
|
+
looksLikeServerless(content) ||
|
|
55
|
+
looksLikeIamPolicy(content));
|
|
50
56
|
}
|
|
51
57
|
export function looksLikePulumiProgram(content) {
|
|
52
58
|
return /(?:^|\n)\s*(?:import\s+\*\s+as\s+pulumi\b|import\s+pulumi\b|from\s+["']@pulumi\/)/.test(content);
|
|
@@ -133,6 +139,7 @@ export function looksLikePulumiYaml(content) {
|
|
|
133
139
|
return (/^\s*resources\s*:/m.test(content) &&
|
|
134
140
|
/^\s+type\s*:\s*[A-Za-z0-9_.-]+:\S+/m.test(content));
|
|
135
141
|
}
|
|
142
|
+
const terraformExpressionWarning = "Terraform expressions are not evaluated. Findings use literal values and declared settings.";
|
|
136
143
|
export function parseIac(content, requested = "auto") {
|
|
137
144
|
if (!content.trim())
|
|
138
145
|
throw new ScanInputError("Content must not be empty or whitespace.");
|
|
@@ -154,6 +161,28 @@ export function parseIac(content, requested = "auto") {
|
|
|
154
161
|
(requested === "auto" && looksLikeHelm(content))) {
|
|
155
162
|
return parseHelm(content);
|
|
156
163
|
}
|
|
164
|
+
if (requested === "serverless" ||
|
|
165
|
+
(requested === "auto" && looksLikeServerless(content))) {
|
|
166
|
+
return parseServerless(content);
|
|
167
|
+
}
|
|
168
|
+
if (requested === "iam" ||
|
|
169
|
+
(requested === "auto" && looksLikeIamPolicy(content))) {
|
|
170
|
+
return parseIamPolicy(content);
|
|
171
|
+
}
|
|
172
|
+
const pipeline = requested === "gitlab" ||
|
|
173
|
+
requested === "azure-pipelines" ||
|
|
174
|
+
requested === "cloudbuild"
|
|
175
|
+
? requested
|
|
176
|
+
: requested === "auto"
|
|
177
|
+
? looksLikePipeline(content)
|
|
178
|
+
: undefined;
|
|
179
|
+
if (pipeline) {
|
|
180
|
+
if (!pipelineMatches(content, pipeline))
|
|
181
|
+
throw new ScanInputError(pipelineShapeError(pipeline));
|
|
182
|
+
return parsePipeline(content, pipeline);
|
|
183
|
+
}
|
|
184
|
+
if (requested === "tfvars")
|
|
185
|
+
return parseTfvars(content);
|
|
157
186
|
if (requested === "bicep" || (requested === "auto" && looksLikeBicep(content))) {
|
|
158
187
|
if (!looksLikeBicep(content))
|
|
159
188
|
throw new ScanInputError("A Bicep file requires a Microsoft resource declaration.");
|
|
@@ -206,14 +235,70 @@ export function parseIac(content, requested = "auto") {
|
|
|
206
235
|
});
|
|
207
236
|
}
|
|
208
237
|
}
|
|
238
|
+
for (const [name, blocks] of Object.entries(object(object(data).module))) {
|
|
239
|
+
const declaration = new RegExp(`\\bmodule\\s+"${escapeRegex(name)}"`).exec(searchable);
|
|
240
|
+
const offset = declaration?.index ?? 0;
|
|
241
|
+
const before = content.slice(0, offset);
|
|
242
|
+
resources.push({
|
|
243
|
+
id: `module.${name}`,
|
|
244
|
+
type: "terraform:module",
|
|
245
|
+
value: object(array(blocks)[0]),
|
|
246
|
+
locate: (path = []) => ({
|
|
247
|
+
line: before.split("\n").length,
|
|
248
|
+
column: offset - before.lastIndexOf("\n"),
|
|
249
|
+
path: ["module", name, ...path].join("."),
|
|
250
|
+
}),
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
for (const [name, blocks] of Object.entries(object(object(data).variable))) {
|
|
254
|
+
const body = object(array(blocks)[0]);
|
|
255
|
+
if (!("default" in body))
|
|
256
|
+
continue;
|
|
257
|
+
const declaration = new RegExp(`\\bvariable\\s+"${escapeRegex(name)}"`).exec(searchable);
|
|
258
|
+
const offset = declaration?.index ?? 0;
|
|
259
|
+
const before = content.slice(0, offset);
|
|
260
|
+
resources.push({
|
|
261
|
+
id: `variable.${name}`,
|
|
262
|
+
type: "terraform:variable",
|
|
263
|
+
value: { name, default: body.default },
|
|
264
|
+
locate: (path = []) => ({
|
|
265
|
+
line: before.split("\n").length,
|
|
266
|
+
column: offset - before.lastIndexOf("\n"),
|
|
267
|
+
path: ["variable", name, ...path].join("."),
|
|
268
|
+
}),
|
|
269
|
+
});
|
|
270
|
+
}
|
|
209
271
|
if (object(data).module)
|
|
210
|
-
warnings.push("External
|
|
272
|
+
warnings.push("External modules are not expanded. The module body was not loaded, and the module source is not fetched or evaluated.");
|
|
273
|
+
const localBlocks = terraformLocalBlocks(data);
|
|
274
|
+
const localDeclarations = [
|
|
275
|
+
...searchable.matchAll(/\blocals\s*(?:"[^"]+"\s*)?\{/g),
|
|
276
|
+
];
|
|
277
|
+
localBlocks.forEach((block, index) => {
|
|
278
|
+
const offset = localDeclarations[index]?.index ?? 0;
|
|
279
|
+
const before = content.slice(0, offset);
|
|
280
|
+
resources.push({
|
|
281
|
+
id: `locals.${index + 1}`,
|
|
282
|
+
type: "terraform:local",
|
|
283
|
+
value: block,
|
|
284
|
+
locate: (path = []) => ({
|
|
285
|
+
line: before.split("\n").length,
|
|
286
|
+
column: offset - before.lastIndexOf("\n"),
|
|
287
|
+
path: ["locals", index, ...path].join("."),
|
|
288
|
+
}),
|
|
289
|
+
});
|
|
290
|
+
});
|
|
211
291
|
if (JSON.stringify(data).includes("${"))
|
|
212
|
-
warnings.push(
|
|
213
|
-
if (
|
|
214
|
-
warnings.
|
|
292
|
+
warnings.push(terraformExpressionWarning);
|
|
293
|
+
if (localBlocks.length > 0 &&
|
|
294
|
+
!warnings.some((warning) => warning.includes("expressions are not evaluated")))
|
|
295
|
+
warnings.push(terraformExpressionWarning);
|
|
296
|
+
if (!resources.some((item) => item.type !== "terraform:module" && item.type !== "terraform:variable"))
|
|
297
|
+
warnings.push("No resource declarations found. Data sources and outputs are not scanned as resources.");
|
|
215
298
|
return { format: "terraform", resources, warnings };
|
|
216
299
|
}
|
|
300
|
+
if (requested === "auto" && looksLikeTfvars(content))
|
|
301
|
+
return parseTfvars(content);
|
|
217
302
|
const lines = new LineCounter();
|
|
218
303
|
const tags = [
|
|
219
304
|
"Ref",
|
|
@@ -379,7 +464,7 @@ export function parseIac(content, requested = "auto") {
|
|
|
379
464
|
}
|
|
380
465
|
}
|
|
381
466
|
if (!format)
|
|
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,
|
|
467
|
+
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, an ARM template, Serverless Framework, or a standalone IAM policy.");
|
|
383
468
|
if (!resources.length)
|
|
384
469
|
warnings.push("No resources found in the submitted document.");
|
|
385
470
|
if (/(?:!(?:Ref|Sub|GetAtt|If)\b|"(?:Ref|Fn::\w+)"\s*:|\$\{)/.test(content))
|
|
@@ -616,6 +701,254 @@ function workflowEnvIsLiteral(value) {
|
|
|
616
701
|
function lineLocation(line, path) {
|
|
617
702
|
return { line, column: 1, path };
|
|
618
703
|
}
|
|
704
|
+
function stolenPipelineInput(content) {
|
|
705
|
+
return (looksLikeGithubWorkflow(content) ||
|
|
706
|
+
looksLikeAnsible(content) ||
|
|
707
|
+
looksLikeHelm(content) ||
|
|
708
|
+
(topLevelKey(content, "apiVersion") && topLevelKey(content, "kind")));
|
|
709
|
+
}
|
|
710
|
+
function azurePipelineShape(content) {
|
|
711
|
+
const poolOrTrigger = topLevelKey(content, "pool") || topLevelKey(content, "trigger");
|
|
712
|
+
const stepsOrStages = topLevelKey(content, "steps") || topLevelKey(content, "stages");
|
|
713
|
+
return poolOrTrigger && stepsOrStages;
|
|
714
|
+
}
|
|
715
|
+
function gitlabShape(content) {
|
|
716
|
+
if (!/\bimage\s*:/.test(content))
|
|
717
|
+
return false;
|
|
718
|
+
if (topLevelKey(content, "stages"))
|
|
719
|
+
return true;
|
|
720
|
+
return /^\s*script\s*:/m.test(content);
|
|
721
|
+
}
|
|
722
|
+
function cloudBuildShape(content) {
|
|
723
|
+
if (!topLevelKey(content, "steps"))
|
|
724
|
+
return false;
|
|
725
|
+
if (topLevelKey(content, "on") && topLevelKey(content, "jobs"))
|
|
726
|
+
return false;
|
|
727
|
+
return /^\s*-\s*name\s*:/m.test(content);
|
|
728
|
+
}
|
|
729
|
+
function looksLikePipeline(content) {
|
|
730
|
+
if (stolenPipelineInput(content) || looksLikeServerless(content))
|
|
731
|
+
return undefined;
|
|
732
|
+
const visible = maskIaCComments(content);
|
|
733
|
+
if (azurePipelineShape(visible))
|
|
734
|
+
return "azure-pipelines";
|
|
735
|
+
if (gitlabShape(visible))
|
|
736
|
+
return "gitlab";
|
|
737
|
+
if (cloudBuildShape(visible))
|
|
738
|
+
return "cloudbuild";
|
|
739
|
+
return undefined;
|
|
740
|
+
}
|
|
741
|
+
function pipelineMatches(content, format) {
|
|
742
|
+
if (stolenPipelineInput(content))
|
|
743
|
+
return false;
|
|
744
|
+
const visible = maskIaCComments(content);
|
|
745
|
+
if (format === "azure-pipelines")
|
|
746
|
+
return azurePipelineShape(visible);
|
|
747
|
+
if (format === "gitlab")
|
|
748
|
+
return gitlabShape(visible);
|
|
749
|
+
return cloudBuildShape(visible);
|
|
750
|
+
}
|
|
751
|
+
function pipelineShapeError(format) {
|
|
752
|
+
if (format === "gitlab")
|
|
753
|
+
return "GitLab CI requires top-level stages or a job with script, and an image.";
|
|
754
|
+
if (format === "azure-pipelines")
|
|
755
|
+
return "Azure Pipelines requires a top-level pool or trigger, plus steps or stages.";
|
|
756
|
+
return "Cloud Build requires top-level steps whose items have a name.";
|
|
757
|
+
}
|
|
758
|
+
const pipelineWarning = "The pipeline is not executed. Only literal text outside comments is checked.";
|
|
759
|
+
function scriptText(value) {
|
|
760
|
+
if (typeof value === "string")
|
|
761
|
+
return value;
|
|
762
|
+
if (Array.isArray(value) && value.every((item) => typeof item === "string"))
|
|
763
|
+
return value.join("\n");
|
|
764
|
+
return undefined;
|
|
765
|
+
}
|
|
766
|
+
function pipelineScriptKey(key) {
|
|
767
|
+
return (key === "script" ||
|
|
768
|
+
key === "before_script" ||
|
|
769
|
+
key === "after_script" ||
|
|
770
|
+
key === "bash" ||
|
|
771
|
+
key === "pwsh" ||
|
|
772
|
+
key === "powershell");
|
|
773
|
+
}
|
|
774
|
+
function pipelinePrintsSecret(script) {
|
|
775
|
+
return script.split(/\n/).some((line) => {
|
|
776
|
+
const code = line.replace(/(^|\s)#.*$/, "").trim();
|
|
777
|
+
if (!/\b(?:echo|printf|print)\b/i.test(code))
|
|
778
|
+
return false;
|
|
779
|
+
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);
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
function collectPipelineHits(value, path, format, hits) {
|
|
783
|
+
if (Array.isArray(value)) {
|
|
784
|
+
value.forEach((item, index) => collectPipelineHits(item, [...path, index], format, hits));
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
if (value === null || typeof value !== "object")
|
|
788
|
+
return;
|
|
789
|
+
for (const [key, child] of Object.entries(object(value))) {
|
|
790
|
+
const childPath = [...path, key];
|
|
791
|
+
if ((key === "image" || key === "container") && typeof child === "string")
|
|
792
|
+
hits.push({ kind: "image", path: childPath, image: child });
|
|
793
|
+
else if (key === "image" &&
|
|
794
|
+
typeof object(child).name === "string")
|
|
795
|
+
hits.push({
|
|
796
|
+
kind: "image",
|
|
797
|
+
path: [...childPath, "name"],
|
|
798
|
+
image: String(object(child).name),
|
|
799
|
+
});
|
|
800
|
+
if (key === "privileged" && child === true)
|
|
801
|
+
hits.push({ kind: "privileged", path: childPath });
|
|
802
|
+
const script = pipelineScriptKey(key) ? scriptText(child) : undefined;
|
|
803
|
+
if (script)
|
|
804
|
+
hits.push({ kind: "script", path: childPath, script });
|
|
805
|
+
if (format === "cloudbuild" && key === "steps" && Array.isArray(child)) {
|
|
806
|
+
child.forEach((step, index) => {
|
|
807
|
+
const name = object(step).name;
|
|
808
|
+
if (typeof name === "string")
|
|
809
|
+
hits.push({
|
|
810
|
+
kind: "image",
|
|
811
|
+
path: [...childPath, index, "name"],
|
|
812
|
+
image: name,
|
|
813
|
+
});
|
|
814
|
+
const args = scriptText(object(step).args);
|
|
815
|
+
if (args)
|
|
816
|
+
hits.push({
|
|
817
|
+
kind: "script",
|
|
818
|
+
path: [...childPath, index, "args"],
|
|
819
|
+
script: args,
|
|
820
|
+
});
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
collectPipelineHits(child, childPath, format, hits);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
function parsePipeline(content, format) {
|
|
827
|
+
const lines = new LineCounter();
|
|
828
|
+
let documents;
|
|
829
|
+
try {
|
|
830
|
+
documents = parseAllDocuments(content, {
|
|
831
|
+
lineCounter: lines,
|
|
832
|
+
prettyErrors: false,
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
catch {
|
|
836
|
+
throw new ScanInputError(pipelineShapeError(format));
|
|
837
|
+
}
|
|
838
|
+
const resources = [];
|
|
839
|
+
let found = false;
|
|
840
|
+
for (const [documentIndex, doc] of documents.entries()) {
|
|
841
|
+
if (doc.errors.length)
|
|
842
|
+
throw new ScanInputError(pipelineShapeError(format));
|
|
843
|
+
let data;
|
|
844
|
+
try {
|
|
845
|
+
data = doc.toJS({ maxAliasCount: 0 });
|
|
846
|
+
}
|
|
847
|
+
catch {
|
|
848
|
+
throw new ScanInputError("YAML aliases are not supported. Expand anchors before scanning.");
|
|
849
|
+
}
|
|
850
|
+
if (data == null)
|
|
851
|
+
continue;
|
|
852
|
+
checkShape(data);
|
|
853
|
+
found = true;
|
|
854
|
+
const hits = [];
|
|
855
|
+
collectPipelineHits(data, [], format, hits);
|
|
856
|
+
const position = (path) => {
|
|
857
|
+
let node = doc.getIn(path, true);
|
|
858
|
+
if (!node?.range)
|
|
859
|
+
node = doc.getIn([], true);
|
|
860
|
+
const at = lines.linePos(node?.range?.[0] ?? doc.range?.[0] ?? 0);
|
|
861
|
+
return {
|
|
862
|
+
line: at.line,
|
|
863
|
+
column: at.col,
|
|
864
|
+
path: [documentIndex, ...path].join("."),
|
|
865
|
+
};
|
|
866
|
+
};
|
|
867
|
+
hits.forEach((hit, index) => {
|
|
868
|
+
if (hit.kind === "image" && hit.image)
|
|
869
|
+
resources.push({
|
|
870
|
+
id: `pipeline.image.${index + 1}`,
|
|
871
|
+
type: "pipeline:image",
|
|
872
|
+
value: { unpinned: imageIsUnpinned(hit.image.trim(), new Set()) },
|
|
873
|
+
locate: () => position(hit.path),
|
|
874
|
+
});
|
|
875
|
+
else if (hit.kind === "script" && hit.script)
|
|
876
|
+
resources.push({
|
|
877
|
+
id: `pipeline.script.${index + 1}`,
|
|
878
|
+
type: "pipeline:script",
|
|
879
|
+
value: { printsSecret: pipelinePrintsSecret(hit.script) },
|
|
880
|
+
locate: () => position(hit.path),
|
|
881
|
+
});
|
|
882
|
+
else if (hit.kind === "privileged")
|
|
883
|
+
resources.push({
|
|
884
|
+
id: `pipeline.privileged.${index + 1}`,
|
|
885
|
+
type: "pipeline:privileged",
|
|
886
|
+
value: { present: true },
|
|
887
|
+
locate: () => position(hit.path),
|
|
888
|
+
});
|
|
889
|
+
});
|
|
890
|
+
}
|
|
891
|
+
if (!found)
|
|
892
|
+
throw new ScanInputError(pipelineShapeError(format));
|
|
893
|
+
return { format, resources, warnings: [pipelineWarning] };
|
|
894
|
+
}
|
|
895
|
+
function looksLikeTfvars(content) {
|
|
896
|
+
const visible = maskIaCComments(content);
|
|
897
|
+
if (/^\s*(?:resource|module|provider)\s+["']/m.test(visible))
|
|
898
|
+
return false;
|
|
899
|
+
if (/^\s*(?:terraform|provider|module)\s*\{/m.test(visible))
|
|
900
|
+
return false;
|
|
901
|
+
return /^\s*[A-Za-z_][A-Za-z0-9_]*\s*=\s*\S/m.test(visible);
|
|
902
|
+
}
|
|
903
|
+
function parseTfvars(content) {
|
|
904
|
+
let data;
|
|
905
|
+
try {
|
|
906
|
+
const [parsed, error] = hcl.parseToObject(content);
|
|
907
|
+
if (error || !parsed)
|
|
908
|
+
throw new Error("parse");
|
|
909
|
+
data = parsed;
|
|
910
|
+
}
|
|
911
|
+
catch {
|
|
912
|
+
throw new ScanInputError("Invalid tfvars. Check assignment syntax.");
|
|
913
|
+
}
|
|
914
|
+
checkShape(data);
|
|
915
|
+
const searchable = maskIaCComments(content);
|
|
916
|
+
const resources = [];
|
|
917
|
+
const skipped = new Set([
|
|
918
|
+
"resource",
|
|
919
|
+
"module",
|
|
920
|
+
"terraform",
|
|
921
|
+
"provider",
|
|
922
|
+
"variable",
|
|
923
|
+
"data",
|
|
924
|
+
"output",
|
|
925
|
+
"locals",
|
|
926
|
+
]);
|
|
927
|
+
for (const [name, value] of Object.entries(object(data))) {
|
|
928
|
+
if (skipped.has(name))
|
|
929
|
+
continue;
|
|
930
|
+
const declaration = new RegExp(`(^|\\n)\\s*${escapeRegex(name)}\\s*=`).exec(searchable);
|
|
931
|
+
const offset = declaration?.index ?? 0;
|
|
932
|
+
const before = content.slice(0, offset);
|
|
933
|
+
resources.push({
|
|
934
|
+
id: `tfvars.${name}`,
|
|
935
|
+
type: "tfvars:assignment",
|
|
936
|
+
value: { name, value },
|
|
937
|
+
locate: (path = []) => ({
|
|
938
|
+
line: before.split("\n").length,
|
|
939
|
+
column: 1,
|
|
940
|
+
path: ["tfvars", name, ...path].join("."),
|
|
941
|
+
}),
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
return {
|
|
945
|
+
format: "tfvars",
|
|
946
|
+
resources,
|
|
947
|
+
warnings: [
|
|
948
|
+
"Terraform variable references are not evaluated. Findings use literal values.",
|
|
949
|
+
],
|
|
950
|
+
};
|
|
951
|
+
}
|
|
619
952
|
function parseDockerfile(content) {
|
|
620
953
|
const instructions = logicalDockerfileLines(content);
|
|
621
954
|
const froms = instructions.flatMap((instruction) => {
|
|
@@ -1070,3 +1403,229 @@ function parseAzure(content, format) {
|
|
|
1070
1403
|
warnings: [azureExpressionWarning],
|
|
1071
1404
|
};
|
|
1072
1405
|
}
|
|
1406
|
+
function terraformLocalBlocks(data) {
|
|
1407
|
+
const raw = object(data).locals;
|
|
1408
|
+
if (Array.isArray(raw))
|
|
1409
|
+
return raw.map((block) => object(block));
|
|
1410
|
+
if (raw && typeof raw === "object") {
|
|
1411
|
+
return Object.values(object(raw)).flatMap((blocks) => array(blocks).map((block) => object(block)));
|
|
1412
|
+
}
|
|
1413
|
+
return [];
|
|
1414
|
+
}
|
|
1415
|
+
export function looksLikeServerless(content) {
|
|
1416
|
+
if (looksLikeGithubWorkflow(content) || looksLikeCompose(content))
|
|
1417
|
+
return false;
|
|
1418
|
+
if (topLevelKey(content, "apiVersion") && topLevelKey(content, "kind"))
|
|
1419
|
+
return false;
|
|
1420
|
+
if (!topLevelKey(content, "service") || !topLevelKey(content, "provider"))
|
|
1421
|
+
return false;
|
|
1422
|
+
return topLevelKey(content, "functions") || topLevelKey(content, "plugins");
|
|
1423
|
+
}
|
|
1424
|
+
export function looksLikeIamPolicy(content) {
|
|
1425
|
+
const trimmed = content.trim();
|
|
1426
|
+
if (!trimmed.startsWith("{"))
|
|
1427
|
+
return false;
|
|
1428
|
+
let value;
|
|
1429
|
+
try {
|
|
1430
|
+
value = JSON.parse(trimmed);
|
|
1431
|
+
}
|
|
1432
|
+
catch {
|
|
1433
|
+
return false;
|
|
1434
|
+
}
|
|
1435
|
+
const root = object(value);
|
|
1436
|
+
if (typeof root.AWSTemplateFormatVersion === "string")
|
|
1437
|
+
return false;
|
|
1438
|
+
if ("resource" in root)
|
|
1439
|
+
return false;
|
|
1440
|
+
const version = root.Version ?? root.version;
|
|
1441
|
+
if (typeof version !== "string" || !version.includes("2012-10-17"))
|
|
1442
|
+
return false;
|
|
1443
|
+
const statement = root.Statement ?? root.statement;
|
|
1444
|
+
return Array.isArray(statement);
|
|
1445
|
+
}
|
|
1446
|
+
const serverlessWarning = "The Serverless Framework config is not executed. Conditions are not evaluated.";
|
|
1447
|
+
const iamPolicyWarning = "The IAM policy is not executed. Conditions are not evaluated.";
|
|
1448
|
+
function serverlessShapeError() {
|
|
1449
|
+
return "Serverless Framework config requires top-level service and provider, plus functions or plugins.";
|
|
1450
|
+
}
|
|
1451
|
+
function iamShapeError() {
|
|
1452
|
+
return "An IAM policy requires Version 2012-10-17 and a Statement array.";
|
|
1453
|
+
}
|
|
1454
|
+
function effectAllows(body) {
|
|
1455
|
+
const effect = body.Effect ?? body.effect;
|
|
1456
|
+
if (effect == null || String(effect).trim() === "")
|
|
1457
|
+
return true;
|
|
1458
|
+
return String(effect).trim().toLowerCase() === "allow";
|
|
1459
|
+
}
|
|
1460
|
+
function actionIsOnlyWildcard(body) {
|
|
1461
|
+
const action = body.Action ?? body.action ?? body.actions ?? body.Actions;
|
|
1462
|
+
if (typeof action === "string")
|
|
1463
|
+
return action.trim() === "*";
|
|
1464
|
+
return (Array.isArray(action) &&
|
|
1465
|
+
action.length > 0 &&
|
|
1466
|
+
action.every((item) => typeof item === "string" && item.trim() === "*"));
|
|
1467
|
+
}
|
|
1468
|
+
function serverlessReference(value) {
|
|
1469
|
+
return /^\$\{[\s\S]+\}$/.test(value.trim());
|
|
1470
|
+
}
|
|
1471
|
+
function yamlLocation(doc, lines, path) {
|
|
1472
|
+
const node = doc.getIn(path, true);
|
|
1473
|
+
const position = lines.linePos(node?.range?.[0] ?? doc.range?.[0] ?? 0);
|
|
1474
|
+
return {
|
|
1475
|
+
line: position.line,
|
|
1476
|
+
column: position.col,
|
|
1477
|
+
path: path.join("."),
|
|
1478
|
+
};
|
|
1479
|
+
}
|
|
1480
|
+
function statementGroups(root) {
|
|
1481
|
+
const groups = [];
|
|
1482
|
+
const seen = new Set();
|
|
1483
|
+
const add = (statements, path) => {
|
|
1484
|
+
if (!Array.isArray(statements) || seen.has(statements))
|
|
1485
|
+
return;
|
|
1486
|
+
seen.add(statements);
|
|
1487
|
+
groups.push({ path, statements });
|
|
1488
|
+
};
|
|
1489
|
+
const provider = object(root.provider);
|
|
1490
|
+
add(provider.iamRoleStatements, ["provider", "iamRoleStatements"]);
|
|
1491
|
+
add(root.iamRoleStatements, ["iamRoleStatements"]);
|
|
1492
|
+
add(object(object(provider.iam).role).statements, ["provider", "iam", "role", "statements"]);
|
|
1493
|
+
add(object(object(root.iam).role).statements, ["iam", "role", "statements"]);
|
|
1494
|
+
if (Array.isArray(provider.iam))
|
|
1495
|
+
add(provider.iam, ["provider", "iam"]);
|
|
1496
|
+
else
|
|
1497
|
+
add(object(provider.iam).statements, ["provider", "iam", "statements"]);
|
|
1498
|
+
return groups;
|
|
1499
|
+
}
|
|
1500
|
+
function parseServerless(content) {
|
|
1501
|
+
if (!looksLikeServerless(content))
|
|
1502
|
+
throw new ScanInputError(serverlessShapeError());
|
|
1503
|
+
const lines = new LineCounter();
|
|
1504
|
+
let documents;
|
|
1505
|
+
try {
|
|
1506
|
+
documents = parseAllDocuments(content, {
|
|
1507
|
+
lineCounter: lines,
|
|
1508
|
+
prettyErrors: false,
|
|
1509
|
+
});
|
|
1510
|
+
}
|
|
1511
|
+
catch {
|
|
1512
|
+
throw new ScanInputError(serverlessShapeError());
|
|
1513
|
+
}
|
|
1514
|
+
const doc = documents.find((item) => item.toJS() != null);
|
|
1515
|
+
if (!doc || doc.errors.length)
|
|
1516
|
+
throw new ScanInputError(serverlessShapeError());
|
|
1517
|
+
let data;
|
|
1518
|
+
try {
|
|
1519
|
+
data = doc.toJS({ maxAliasCount: 0 });
|
|
1520
|
+
}
|
|
1521
|
+
catch {
|
|
1522
|
+
throw new ScanInputError("YAML aliases are not supported. Expand anchors before scanning.");
|
|
1523
|
+
}
|
|
1524
|
+
const root = object(data);
|
|
1525
|
+
if (root.service == null ||
|
|
1526
|
+
root.provider == null ||
|
|
1527
|
+
(root.functions == null && root.plugins == null))
|
|
1528
|
+
throw new ScanInputError(serverlessShapeError());
|
|
1529
|
+
const resources = [];
|
|
1530
|
+
let iamCount = 0;
|
|
1531
|
+
for (const group of statementGroups(root)) {
|
|
1532
|
+
group.statements.forEach((statement, index) => {
|
|
1533
|
+
const body = object(statement);
|
|
1534
|
+
if (!effectAllows(body) || !actionIsOnlyWildcard(body))
|
|
1535
|
+
return;
|
|
1536
|
+
iamCount += 1;
|
|
1537
|
+
const path = [...group.path, index, "Action"];
|
|
1538
|
+
resources.push({
|
|
1539
|
+
id: `serverless.iam.${iamCount}`,
|
|
1540
|
+
type: "serverless:iam",
|
|
1541
|
+
value: { wildcard: true },
|
|
1542
|
+
locate: () => yamlLocation(doc, lines, path),
|
|
1543
|
+
});
|
|
1544
|
+
});
|
|
1545
|
+
}
|
|
1546
|
+
if (iamCount === 0) {
|
|
1547
|
+
resources.push({
|
|
1548
|
+
id: "serverless.iam",
|
|
1549
|
+
type: "serverless:iam",
|
|
1550
|
+
value: { wildcard: false },
|
|
1551
|
+
locate: () => yamlLocation(doc, lines, ["provider", "iamRoleStatements"]),
|
|
1552
|
+
});
|
|
1553
|
+
}
|
|
1554
|
+
const envHits = [];
|
|
1555
|
+
for (const [key, value] of Object.entries(object(object(root.provider).environment))) {
|
|
1556
|
+
if (secretVariableName(key))
|
|
1557
|
+
envHits.push({ path: ["provider", "environment", key], key, value });
|
|
1558
|
+
}
|
|
1559
|
+
for (const [name, raw] of Object.entries(object(root.functions))) {
|
|
1560
|
+
for (const [key, value] of Object.entries(object(object(raw).environment))) {
|
|
1561
|
+
if (secretVariableName(key))
|
|
1562
|
+
envHits.push({ path: ["functions", name, "environment", key], key, value });
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
if (envHits.length === 0) {
|
|
1566
|
+
resources.push({
|
|
1567
|
+
id: "serverless.environment",
|
|
1568
|
+
type: "serverless:env",
|
|
1569
|
+
value: { literal: false },
|
|
1570
|
+
locate: () => yamlLocation(doc, lines, ["provider", "environment"]),
|
|
1571
|
+
});
|
|
1572
|
+
}
|
|
1573
|
+
else {
|
|
1574
|
+
envHits.forEach((hit, index) => {
|
|
1575
|
+
const literal = typeof hit.value === "string" &&
|
|
1576
|
+
hit.value.trim() !== "" &&
|
|
1577
|
+
!serverlessReference(hit.value);
|
|
1578
|
+
resources.push({
|
|
1579
|
+
id: `serverless.env.${index + 1}`,
|
|
1580
|
+
type: "serverless:env",
|
|
1581
|
+
value: { literal },
|
|
1582
|
+
locate: () => yamlLocation(doc, lines, hit.path),
|
|
1583
|
+
});
|
|
1584
|
+
});
|
|
1585
|
+
}
|
|
1586
|
+
return { format: "serverless", resources, warnings: [serverlessWarning] };
|
|
1587
|
+
}
|
|
1588
|
+
function parseIamPolicy(content) {
|
|
1589
|
+
if (!looksLikeIamPolicy(content))
|
|
1590
|
+
throw new ScanInputError(iamShapeError());
|
|
1591
|
+
let root;
|
|
1592
|
+
try {
|
|
1593
|
+
root = object(JSON.parse(content));
|
|
1594
|
+
}
|
|
1595
|
+
catch {
|
|
1596
|
+
throw new ScanInputError(iamShapeError());
|
|
1597
|
+
}
|
|
1598
|
+
const statements = array(root.Statement ?? root.statement);
|
|
1599
|
+
const resources = [];
|
|
1600
|
+
statements.forEach((statement, index) => {
|
|
1601
|
+
const body = object(statement);
|
|
1602
|
+
if (!effectAllows(body) || !actionIsOnlyWildcard(body))
|
|
1603
|
+
return;
|
|
1604
|
+
const line = iamStatementLine(content, index);
|
|
1605
|
+
resources.push({
|
|
1606
|
+
id: `iam.statement.${index + 1}`,
|
|
1607
|
+
type: "iam:statement",
|
|
1608
|
+
value: { wildcard: true },
|
|
1609
|
+
locate: () => lineLocation(line, `Statement.${index}.Action`),
|
|
1610
|
+
});
|
|
1611
|
+
});
|
|
1612
|
+
if (!resources.length) {
|
|
1613
|
+
resources.push({
|
|
1614
|
+
id: "iam.policy",
|
|
1615
|
+
type: "iam:statement",
|
|
1616
|
+
value: { wildcard: false },
|
|
1617
|
+
locate: () => lineLocation(1, "Statement"),
|
|
1618
|
+
});
|
|
1619
|
+
}
|
|
1620
|
+
return { format: "iam", resources, warnings: [iamPolicyWarning] };
|
|
1621
|
+
}
|
|
1622
|
+
function iamStatementLine(content, index) {
|
|
1623
|
+
const matches = [...content.matchAll(/"Statement"\s*:/g)];
|
|
1624
|
+
const start = matches[0]?.index ?? 0;
|
|
1625
|
+
const region = content.slice(start);
|
|
1626
|
+
const actions = [...region.matchAll(/"Action"\s*:/g)];
|
|
1627
|
+
const action = actions[index];
|
|
1628
|
+
if (!action || action.index == null)
|
|
1629
|
+
return lineAt(content, start);
|
|
1630
|
+
return lineAt(content, start + action.index);
|
|
1631
|
+
}
|
package/dist/iac/rules.js
CHANGED
|
@@ -323,6 +323,48 @@ export const rules = {
|
|
|
323
323
|
detail: "A cidr or source allows 0.0.0.0/0.",
|
|
324
324
|
remediation: "Restrict the cidr or source to approved addresses.",
|
|
325
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
|
+
},
|
|
354
|
+
TF002: {
|
|
355
|
+
category: "security",
|
|
356
|
+
severity: "high",
|
|
357
|
+
title: "Secret assigned in a Terraform local",
|
|
358
|
+
detail: "A Terraform local assigns a non-empty literal to a secret-like name.",
|
|
359
|
+
remediation: "Pass the value from a secret store or a variable reference. Do not commit a literal secret in locals.",
|
|
360
|
+
},
|
|
361
|
+
SLS001: {
|
|
362
|
+
category: "security",
|
|
363
|
+
severity: "high",
|
|
364
|
+
title: "Secret assigned in serverless environment",
|
|
365
|
+
detail: "A Serverless Framework environment entry assigns a non-empty literal to a secret-like name.",
|
|
366
|
+
remediation: "Reference the value with a Serverless variable such as ${env:TOKEN}. Do not commit a literal secret.",
|
|
367
|
+
},
|
|
326
368
|
};
|
|
327
369
|
export function resourceFindings(resources, format) {
|
|
328
370
|
const findings = [];
|
|
@@ -358,6 +400,36 @@ export function resourceFindings(resources, format) {
|
|
|
358
400
|
checkAzure(resource, check);
|
|
359
401
|
continue;
|
|
360
402
|
}
|
|
403
|
+
if (format === "gitlab" ||
|
|
404
|
+
format === "azure-pipelines" ||
|
|
405
|
+
format === "cloudbuild") {
|
|
406
|
+
checkPipeline(resource, check);
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
if (format === "serverless") {
|
|
410
|
+
checkServerless(resource, check);
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
if (format === "iam") {
|
|
414
|
+
check("IAM001", resource.value.wildcard === true, ["Action"]);
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
if (resource.type === "terraform:local") {
|
|
418
|
+
checkTerraformLiterals(resource.value, "local", check, []);
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
if (resource.type === "terraform:module") {
|
|
422
|
+
checkTerraformLiterals(resource.value, "module", check, []);
|
|
423
|
+
continue;
|
|
424
|
+
}
|
|
425
|
+
if (resource.type === "terraform:variable") {
|
|
426
|
+
checkTerraformLiterals({ [String(resource.value.name ?? "default")]: resource.value.default }, "assignment", check, ["default"]);
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
if (format === "tfvars" || resource.type === "tfvars:assignment") {
|
|
430
|
+
checkTerraformLiterals({ [String(resource.value.name ?? "value")]: resource.value.value }, "assignment", check, []);
|
|
431
|
+
continue;
|
|
432
|
+
}
|
|
361
433
|
walk(resource.value, (key, value, path, parent) => {
|
|
362
434
|
if (typeof value !== "string")
|
|
363
435
|
return;
|
|
@@ -419,6 +491,58 @@ function checkAnsible(resource, check) {
|
|
|
419
491
|
else if (resource.type === "ansible:cidr")
|
|
420
492
|
check("ANS002", resource.value.present === true, ["cidr"]);
|
|
421
493
|
}
|
|
494
|
+
function checkPipeline(resource, check) {
|
|
495
|
+
if (resource.type === "pipeline:image")
|
|
496
|
+
check("PIPE001", resource.value.unpinned === true, ["image"]);
|
|
497
|
+
else if (resource.type === "pipeline:script")
|
|
498
|
+
check("PIPE002", resource.value.printsSecret === true, ["script"]);
|
|
499
|
+
else if (resource.type === "pipeline:privileged")
|
|
500
|
+
check("PIPE003", resource.value.present === true, ["privileged"]);
|
|
501
|
+
}
|
|
502
|
+
function checkServerless(resource, check) {
|
|
503
|
+
if (resource.type === "serverless:iam")
|
|
504
|
+
check("IAM001", resource.value.wildcard === true, ["Action"]);
|
|
505
|
+
else if (resource.type === "serverless:env")
|
|
506
|
+
check("SLS001", resource.value.literal === true, ["environment"]);
|
|
507
|
+
}
|
|
508
|
+
function terraformReference(value) {
|
|
509
|
+
const trimmed = value.trim();
|
|
510
|
+
return trimmed.includes("${") || /^var\./.test(trimmed);
|
|
511
|
+
}
|
|
512
|
+
function secretAssignmentName(name) {
|
|
513
|
+
const normalized = name.replace(/-/g, "_");
|
|
514
|
+
return (/(?:^|_)(?:password|passwd|secret|token)$/i.test(normalized) ||
|
|
515
|
+
/(?:^|_)(?:api_?key|auth_token|access_key|private_key)$/i.test(normalized));
|
|
516
|
+
}
|
|
517
|
+
function moduleCidrExempt(name) {
|
|
518
|
+
const normalized = name.trim().toLowerCase().replace(/[\s-]+/g, "_");
|
|
519
|
+
return (normalized === "egress" ||
|
|
520
|
+
normalized === "destination" ||
|
|
521
|
+
normalized === "ipv6_egress");
|
|
522
|
+
}
|
|
523
|
+
function checkTerraformLiterals(value, mode, check, path, name = "") {
|
|
524
|
+
if (typeof value === "string" || typeof value === "boolean") {
|
|
525
|
+
const open = value === "0.0.0.0/0" || value === "::/0";
|
|
526
|
+
const exempt = mode === "assignment" ? name.toLowerCase() === "egress" : moduleCidrExempt(name);
|
|
527
|
+
if (typeof value === "string")
|
|
528
|
+
check("NET001", open && name !== "" && !exempt, path);
|
|
529
|
+
if (name === "publicly_accessible")
|
|
530
|
+
check("DB001", value === true, path);
|
|
531
|
+
if (mode === "assignment" && typeof value === "string" && secretAssignmentName(name))
|
|
532
|
+
check("TF001", value.trim() !== "" && !terraformReference(value), path);
|
|
533
|
+
if (mode === "local" && typeof value === "string" && secretAssignmentName(name))
|
|
534
|
+
check("TF002", value.trim() !== "" && !terraformReference(value), path);
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
537
|
+
if (Array.isArray(value)) {
|
|
538
|
+
value.forEach((item, index) => checkTerraformLiterals(item, mode, check, [...path, index], name));
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
if (value && typeof value === "object") {
|
|
542
|
+
for (const [key, child] of Object.entries(value))
|
|
543
|
+
checkTerraformLiterals(child, mode, check, [...path, key], key);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
422
546
|
function checkAzure(resource, check) {
|
|
423
547
|
if (resource.type === "azure:exposure")
|
|
424
548
|
check("AZ001", resource.value.present === true, ["sourceAddressPrefix"]);
|
package/dist/server.js
CHANGED
|
@@ -79,12 +79,18 @@ export function createServer(remote = false) {
|
|
|
79
79
|
"ansible",
|
|
80
80
|
"bicep",
|
|
81
81
|
"arm",
|
|
82
|
+
"gitlab",
|
|
83
|
+
"azure-pipelines",
|
|
84
|
+
"cloudbuild",
|
|
85
|
+
"tfvars",
|
|
86
|
+
"serverless",
|
|
87
|
+
"iam",
|
|
82
88
|
"application",
|
|
83
89
|
"auto",
|
|
84
90
|
])
|
|
85
91
|
.optional()
|
|
86
92
|
.default("auto")
|
|
87
|
-
.describe("Content format: terraform, cloudformation, kubernetes, pulumi, compose, dockerfile, github, helm, ansible, bicep, arm, application, auto."),
|
|
93
|
+
.describe("Content format: terraform, cloudformation, kubernetes, pulumi, compose, dockerfile, github, helm, ansible, bicep, arm, gitlab, azure-pipelines, cloudbuild, tfvars, serverless, iam, application, auto."),
|
|
88
94
|
},
|
|
89
95
|
}, async ({ content, focus, format, minSeverity, maxFindings }) => {
|
|
90
96
|
const botHints = fetchPublicBotHints();
|
|
@@ -132,7 +138,7 @@ export function createServer(remote = false) {
|
|
|
132
138
|
description: (name === "scan_iac_stub"
|
|
133
139
|
? "Compatibility alias for scan_iac; runs the full scanner. "
|
|
134
140
|
: "") +
|
|
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
|
|
141
|
+
"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, tfvars, Serverless Framework, and a standalone IAM policy. Helm templates, Ansible facts, Bicep modules, ARM expressions, pipelines, Serverless Framework config, and IAM policies are not executed. Conditions are not evaluated. Terraform module sources are not fetched. Check security, reliability and cost rules with resource locations and remediation. Pulumi programs are scanned as text.",
|
|
136
142
|
annotations: {
|
|
137
143
|
readOnlyHint: true,
|
|
138
144
|
destructiveHint: false,
|
|
@@ -140,7 +146,7 @@ export function createServer(remote = false) {
|
|
|
140
146
|
},
|
|
141
147
|
_meta: { securitySchemes: [{ type: "oauth2", scopes: ["mcp"] }] },
|
|
142
148
|
inputSchema: {
|
|
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
|
|
149
|
+
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, tfvars, Serverless Framework, and a standalone IAM policy."),
|
|
144
150
|
format: z
|
|
145
151
|
.enum([
|
|
146
152
|
"terraform",
|
|
@@ -154,6 +160,12 @@ export function createServer(remote = false) {
|
|
|
154
160
|
"ansible",
|
|
155
161
|
"bicep",
|
|
156
162
|
"arm",
|
|
163
|
+
"gitlab",
|
|
164
|
+
"azure-pipelines",
|
|
165
|
+
"cloudbuild",
|
|
166
|
+
"tfvars",
|
|
167
|
+
"serverless",
|
|
168
|
+
"iam",
|
|
157
169
|
"auto",
|
|
158
170
|
])
|
|
159
171
|
.default("auto"),
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.6.
|
|
1
|
+
export declare const VERSION = "0.6.13";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.6.
|
|
1
|
+
export const VERSION = "0.6.13";
|