@skaleagents/swarm 0.6.11 → 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/dist/iac/parse.d.ts +1 -1
- package/dist/iac/parse.js +303 -4
- package/dist/iac/rules.js +90 -0
- package/dist/server.js +11 -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";
|
|
2
2
|
export type Path = (string | number)[];
|
|
3
3
|
export type Location = {
|
|
4
4
|
line: number;
|
package/dist/iac/parse.js
CHANGED
|
@@ -46,7 +46,9 @@ export function looksLikeIac(content) {
|
|
|
46
46
|
looksLikeHelm(content) ||
|
|
47
47
|
looksLikeAnsible(content) ||
|
|
48
48
|
looksLikeBicep(content) ||
|
|
49
|
-
looksLikeArm(content)
|
|
49
|
+
looksLikeArm(content) ||
|
|
50
|
+
looksLikePipeline(content) !== undefined ||
|
|
51
|
+
looksLikeTfvars(content));
|
|
50
52
|
}
|
|
51
53
|
export function looksLikePulumiProgram(content) {
|
|
52
54
|
return /(?:^|\n)\s*(?:import\s+\*\s+as\s+pulumi\b|import\s+pulumi\b|from\s+["']@pulumi\/)/.test(content);
|
|
@@ -154,6 +156,20 @@ export function parseIac(content, requested = "auto") {
|
|
|
154
156
|
(requested === "auto" && looksLikeHelm(content))) {
|
|
155
157
|
return parseHelm(content);
|
|
156
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);
|
|
157
173
|
if (requested === "bicep" || (requested === "auto" && looksLikeBicep(content))) {
|
|
158
174
|
if (!looksLikeBicep(content))
|
|
159
175
|
throw new ScanInputError("A Bicep file requires a Microsoft resource declaration.");
|
|
@@ -206,14 +222,49 @@ export function parseIac(content, requested = "auto") {
|
|
|
206
222
|
});
|
|
207
223
|
}
|
|
208
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
|
+
}
|
|
209
258
|
if (object(data).module)
|
|
210
|
-
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.");
|
|
211
260
|
if (JSON.stringify(data).includes("${"))
|
|
212
261
|
warnings.push("Terraform expressions are not evaluated. Findings use literal values and declared settings.");
|
|
213
|
-
if (!resources.
|
|
214
|
-
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.");
|
|
215
264
|
return { format: "terraform", resources, warnings };
|
|
216
265
|
}
|
|
266
|
+
if (requested === "auto" && looksLikeTfvars(content))
|
|
267
|
+
return parseTfvars(content);
|
|
217
268
|
const lines = new LineCounter();
|
|
218
269
|
const tags = [
|
|
219
270
|
"Ref",
|
|
@@ -616,6 +667,254 @@ function workflowEnvIsLiteral(value) {
|
|
|
616
667
|
function lineLocation(line, path) {
|
|
617
668
|
return { line, column: 1, path };
|
|
618
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
|
+
}
|
|
619
918
|
function parseDockerfile(content) {
|
|
620
919
|
const instructions = logicalDockerfileLines(content);
|
|
621
920
|
const froms = instructions.flatMap((instruction) => {
|
package/dist/iac/rules.js
CHANGED
|
@@ -323,6 +323,34 @@ 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
|
+
},
|
|
326
354
|
};
|
|
327
355
|
export function resourceFindings(resources, format) {
|
|
328
356
|
const findings = [];
|
|
@@ -358,6 +386,24 @@ export function resourceFindings(resources, format) {
|
|
|
358
386
|
checkAzure(resource, check);
|
|
359
387
|
continue;
|
|
360
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
|
+
}
|
|
361
407
|
walk(resource.value, (key, value, path, parent) => {
|
|
362
408
|
if (typeof value !== "string")
|
|
363
409
|
return;
|
|
@@ -419,6 +465,50 @@ function checkAnsible(resource, check) {
|
|
|
419
465
|
else if (resource.type === "ansible:cidr")
|
|
420
466
|
check("ANS002", resource.value.present === true, ["cidr"]);
|
|
421
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
|
+
}
|
|
422
512
|
function checkAzure(resource, check) {
|
|
423
513
|
if (resource.type === "azure:exposure")
|
|
424
514
|
check("AZ001", resource.value.present === true, ["sourceAddressPrefix"]);
|
package/dist/server.js
CHANGED
|
@@ -79,12 +79,16 @@ export function createServer(remote = false) {
|
|
|
79
79
|
"ansible",
|
|
80
80
|
"bicep",
|
|
81
81
|
"arm",
|
|
82
|
+
"gitlab",
|
|
83
|
+
"azure-pipelines",
|
|
84
|
+
"cloudbuild",
|
|
85
|
+
"tfvars",
|
|
82
86
|
"application",
|
|
83
87
|
"auto",
|
|
84
88
|
])
|
|
85
89
|
.optional()
|
|
86
90
|
.default("auto")
|
|
87
|
-
.describe("Content format: terraform, cloudformation, kubernetes, pulumi, compose, dockerfile, github, helm, ansible, bicep, arm, application, auto."),
|
|
91
|
+
.describe("Content format: terraform, cloudformation, kubernetes, pulumi, compose, dockerfile, github, helm, ansible, bicep, arm, gitlab, azure-pipelines, cloudbuild, tfvars, application, auto."),
|
|
88
92
|
},
|
|
89
93
|
}, async ({ content, focus, format, minSeverity, maxFindings }) => {
|
|
90
94
|
const botHints = fetchPublicBotHints();
|
|
@@ -132,7 +136,7 @@ export function createServer(remote = false) {
|
|
|
132
136
|
description: (name === "scan_iac_stub"
|
|
133
137
|
? "Compatibility alias for scan_iac; runs the full scanner. "
|
|
134
138
|
: "") +
|
|
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,
|
|
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.",
|
|
136
140
|
annotations: {
|
|
137
141
|
readOnlyHint: true,
|
|
138
142
|
destructiveHint: false,
|
|
@@ -140,7 +144,7 @@ export function createServer(remote = false) {
|
|
|
140
144
|
},
|
|
141
145
|
_meta: { securitySchemes: [{ type: "oauth2", scopes: ["mcp"] }] },
|
|
142
146
|
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,
|
|
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."),
|
|
144
148
|
format: z
|
|
145
149
|
.enum([
|
|
146
150
|
"terraform",
|
|
@@ -154,6 +158,10 @@ export function createServer(remote = false) {
|
|
|
154
158
|
"ansible",
|
|
155
159
|
"bicep",
|
|
156
160
|
"arm",
|
|
161
|
+
"gitlab",
|
|
162
|
+
"azure-pipelines",
|
|
163
|
+
"cloudbuild",
|
|
164
|
+
"tfvars",
|
|
157
165
|
"auto",
|
|
158
166
|
])
|
|
159
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";
|