@skaleagents/swarm 0.6.14 → 0.6.15
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 -0
- package/dist/iac/parse.js +55 -2
- package/dist/iac/rules.js +227 -38
- 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
|
@@ -33,5 +33,6 @@ 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 terraformJsonencodeValue(expression: string): unknown | undefined;
|
|
36
37
|
export declare function looksLikeServerless(content: string): boolean;
|
|
37
38
|
export declare function looksLikeIamPolicy(content: string): boolean;
|
package/dist/iac/parse.js
CHANGED
|
@@ -139,7 +139,7 @@ export function looksLikePulumiYaml(content) {
|
|
|
139
139
|
return (/^\s*resources\s*:/m.test(content) &&
|
|
140
140
|
/^\s+type\s*:\s*[A-Za-z0-9_.-]+:\S+/m.test(content));
|
|
141
141
|
}
|
|
142
|
-
const terraformExpressionWarning = "Same-file variable and local literals are resolved.
|
|
142
|
+
const terraformExpressionWarning = "Same-file variable and local literals are resolved. A string of those references and format of known literals are resolved. Other functions and expressions are not evaluated.";
|
|
143
143
|
export function parseIac(content, requested = "auto") {
|
|
144
144
|
if (!content.trim())
|
|
145
145
|
throw new ScanInputError("Content must not be empty or whitespace.");
|
|
@@ -470,7 +470,7 @@ export function parseIac(content, requested = "auto") {
|
|
|
470
470
|
warnings.push("No resources found in the submitted document.");
|
|
471
471
|
if (format === "cloudformation" &&
|
|
472
472
|
/(?:!(?:Ref|Sub|GetAtt|If)\b|"(?:Ref|Fn::\w+)"\s*:|\bRef\s*:)/.test(content))
|
|
473
|
-
warnings.push("Same-file parameter defaults are resolved.
|
|
473
|
+
warnings.push("Same-file parameter defaults are resolved. A simple Fn::Sub of those parameters is resolved. Other functions are not evaluated.");
|
|
474
474
|
else if (/(?:!(?:Ref|Sub|GetAtt|If)\b|"(?:Ref|Fn::\w+)"\s*:|\$\{)/.test(content))
|
|
475
475
|
warnings.push("Intrinsic functions and expressions are not evaluated. Only literal configuration is checked.");
|
|
476
476
|
return { format, resources, warnings };
|
|
@@ -1445,6 +1445,36 @@ function cloudFormationIntrinsic(value) {
|
|
|
1445
1445
|
return false;
|
|
1446
1446
|
return Object.keys(value).some((key) => key === "Fn::Sub" || key === "Fn::Join" || key === "Fn::FindInMap" || key.startsWith("Fn::"));
|
|
1447
1447
|
}
|
|
1448
|
+
function cloudFormationSubTemplate(value) {
|
|
1449
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
1450
|
+
return undefined;
|
|
1451
|
+
const entries = Object.entries(value).filter(([key]) => key !== "__referencedSecrets");
|
|
1452
|
+
if (entries.length !== 1)
|
|
1453
|
+
return undefined;
|
|
1454
|
+
const [key, template] = entries[0];
|
|
1455
|
+
if (key !== "Fn::Sub" || typeof template !== "string")
|
|
1456
|
+
return undefined;
|
|
1457
|
+
return template;
|
|
1458
|
+
}
|
|
1459
|
+
function substituteKnownParameters(template, parameters) {
|
|
1460
|
+
let unresolved = false;
|
|
1461
|
+
let secret = false;
|
|
1462
|
+
const text = template.replace(/\$\{(!?)([^}]*)\}/g, (full, literalMark, rawName) => {
|
|
1463
|
+
if (literalMark === "!")
|
|
1464
|
+
return `\${${rawName}}`;
|
|
1465
|
+
const name = rawName.trim();
|
|
1466
|
+
if (!parameters.defaults.has(name)) {
|
|
1467
|
+
unresolved = true;
|
|
1468
|
+
return full;
|
|
1469
|
+
}
|
|
1470
|
+
if (parameters.secretNames.has(name))
|
|
1471
|
+
secret = true;
|
|
1472
|
+
return String(parameters.defaults.get(name));
|
|
1473
|
+
});
|
|
1474
|
+
if (unresolved)
|
|
1475
|
+
return undefined;
|
|
1476
|
+
return { text, secret };
|
|
1477
|
+
}
|
|
1448
1478
|
function resolveCloudFormationRefs(value, parameters) {
|
|
1449
1479
|
const secretPaths = [];
|
|
1450
1480
|
const resolved = resolveCloudFormationValue(value, parameters, [], secretPaths);
|
|
@@ -1472,6 +1502,15 @@ function resolveCloudFormationValue(value, parameters, path, secretPaths) {
|
|
|
1472
1502
|
secretPaths.push(path);
|
|
1473
1503
|
return literal;
|
|
1474
1504
|
}
|
|
1505
|
+
const subTemplate = cloudFormationSubTemplate(value);
|
|
1506
|
+
if (subTemplate !== undefined) {
|
|
1507
|
+
const substituted = substituteKnownParameters(subTemplate, parameters);
|
|
1508
|
+
if (!substituted)
|
|
1509
|
+
return value;
|
|
1510
|
+
if (substituted.secret)
|
|
1511
|
+
secretPaths.push(path);
|
|
1512
|
+
return substituted.text;
|
|
1513
|
+
}
|
|
1475
1514
|
if (cloudFormationIntrinsic(value))
|
|
1476
1515
|
return value;
|
|
1477
1516
|
if (Array.isArray(value))
|
|
@@ -1487,6 +1526,20 @@ function resolveCloudFormationValue(value, parameters, path, secretPaths) {
|
|
|
1487
1526
|
}
|
|
1488
1527
|
return value;
|
|
1489
1528
|
}
|
|
1529
|
+
export function terraformJsonencodeValue(expression) {
|
|
1530
|
+
const match = /^\$\{jsonencode\(([\s\S]*)\)\}$/.exec(expression.trim());
|
|
1531
|
+
if (!match)
|
|
1532
|
+
return undefined;
|
|
1533
|
+
try {
|
|
1534
|
+
const [parsed, error] = hcl.parseToObject(`value = ${match[1]}\n`);
|
|
1535
|
+
if (error || !parsed)
|
|
1536
|
+
return undefined;
|
|
1537
|
+
return object(parsed).value;
|
|
1538
|
+
}
|
|
1539
|
+
catch {
|
|
1540
|
+
return undefined;
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1490
1543
|
function terraformLocalBlocks(data) {
|
|
1491
1544
|
const raw = object(data).locals;
|
|
1492
1545
|
if (Array.isArray(raw))
|
package/dist/iac/rules.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { array, object } from "./parse.js";
|
|
1
|
+
import { array, object, terraformJsonencodeValue } from "./parse.js";
|
|
2
2
|
import { externalHttp, literalCredential } from "../review.js";
|
|
3
3
|
export const rules = {
|
|
4
4
|
SEC001: {
|
|
@@ -40,8 +40,15 @@ export const rules = {
|
|
|
40
40
|
category: "security",
|
|
41
41
|
severity: "high",
|
|
42
42
|
title: "Wildcard permission detected",
|
|
43
|
-
detail: "An Allow statement grants wildcard
|
|
44
|
-
remediation: "List the required actions
|
|
43
|
+
detail: "An Allow statement grants a wildcard action.",
|
|
44
|
+
remediation: "List the required actions explicitly, with resource and condition restrictions.",
|
|
45
|
+
},
|
|
46
|
+
IAM002: {
|
|
47
|
+
category: "security",
|
|
48
|
+
severity: "high",
|
|
49
|
+
title: "Policy allows any principal",
|
|
50
|
+
detail: "An Allow statement sets Principal to any AWS principal.",
|
|
51
|
+
remediation: "Name the trusted principals explicitly instead of allowing any principal.",
|
|
45
52
|
},
|
|
46
53
|
DATA001: {
|
|
47
54
|
category: "security",
|
|
@@ -573,6 +580,150 @@ function checkTerraformLiterals(value, mode, check, path, name = "", referencedS
|
|
|
573
580
|
}
|
|
574
581
|
}
|
|
575
582
|
const exactSameFileRef = /^\$\{(var|local)\.([A-Za-z_][A-Za-z0-9_]*)\}$/;
|
|
583
|
+
const interpolationPiece = /\$\{(var|local)\.([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
|
584
|
+
const interpolationOnly = /^(\$\{(?:var|local)\.[A-Za-z_][A-Za-z0-9_]*\})+$/;
|
|
585
|
+
function sameFileText(kind, name, sameFile) {
|
|
586
|
+
const literal = (kind === "var" ? sameFile.vars : sameFile.locals).get(name);
|
|
587
|
+
if (typeof literal === "string" || typeof literal === "number" || typeof literal === "boolean")
|
|
588
|
+
return String(literal);
|
|
589
|
+
return undefined;
|
|
590
|
+
}
|
|
591
|
+
function resolveInterpolationString(value, sameFile) {
|
|
592
|
+
const trimmed = value.trim();
|
|
593
|
+
if (!interpolationOnly.test(trimmed))
|
|
594
|
+
return undefined;
|
|
595
|
+
let count = 0;
|
|
596
|
+
let failed = false;
|
|
597
|
+
const text = trimmed.replace(interpolationPiece, (_full, kind, name) => {
|
|
598
|
+
count += 1;
|
|
599
|
+
const part = sameFileText(kind, name, sameFile);
|
|
600
|
+
if (part === undefined) {
|
|
601
|
+
failed = true;
|
|
602
|
+
return "";
|
|
603
|
+
}
|
|
604
|
+
return part;
|
|
605
|
+
});
|
|
606
|
+
if (failed || count < 2)
|
|
607
|
+
return undefined;
|
|
608
|
+
return text;
|
|
609
|
+
}
|
|
610
|
+
function splitCallArguments(source) {
|
|
611
|
+
const args = [];
|
|
612
|
+
let current = "";
|
|
613
|
+
let quote = null;
|
|
614
|
+
let depth = 0;
|
|
615
|
+
for (let index = 0; index < source.length; index += 1) {
|
|
616
|
+
const char = source[index];
|
|
617
|
+
if (quote) {
|
|
618
|
+
current += char;
|
|
619
|
+
if (char === "\\" && index + 1 < source.length) {
|
|
620
|
+
current += source[index + 1];
|
|
621
|
+
index += 1;
|
|
622
|
+
continue;
|
|
623
|
+
}
|
|
624
|
+
if (char === quote)
|
|
625
|
+
quote = null;
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
if (char === '"' || char === "'") {
|
|
629
|
+
quote = char;
|
|
630
|
+
current += char;
|
|
631
|
+
continue;
|
|
632
|
+
}
|
|
633
|
+
if (char === "(" || char === "[" || char === "{") {
|
|
634
|
+
depth += 1;
|
|
635
|
+
current += char;
|
|
636
|
+
continue;
|
|
637
|
+
}
|
|
638
|
+
if (char === ")" || char === "]" || char === "}") {
|
|
639
|
+
if (depth === 0)
|
|
640
|
+
return undefined;
|
|
641
|
+
depth -= 1;
|
|
642
|
+
current += char;
|
|
643
|
+
continue;
|
|
644
|
+
}
|
|
645
|
+
if (char === "," && depth === 0) {
|
|
646
|
+
args.push(current.trim());
|
|
647
|
+
current = "";
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
current += char;
|
|
651
|
+
}
|
|
652
|
+
if (quote || depth !== 0)
|
|
653
|
+
return undefined;
|
|
654
|
+
if (current.trim() !== "")
|
|
655
|
+
args.push(current.trim());
|
|
656
|
+
return args;
|
|
657
|
+
}
|
|
658
|
+
function unquoteHcl(token) {
|
|
659
|
+
if (token.length < 2)
|
|
660
|
+
return undefined;
|
|
661
|
+
const quote = token[0];
|
|
662
|
+
if ((quote !== '"' && quote !== "'") || token[token.length - 1] !== quote)
|
|
663
|
+
return undefined;
|
|
664
|
+
const body = token.slice(1, -1);
|
|
665
|
+
return quote === '"'
|
|
666
|
+
? body.replace(/\\n/g, "\n").replace(/\\"/g, '"').replace(/\\\\/g, "\\")
|
|
667
|
+
: body.replace(/\\'/g, "'").replace(/\\\\/g, "\\");
|
|
668
|
+
}
|
|
669
|
+
function resolveFormatArgument(token, sameFile) {
|
|
670
|
+
const literal = unquoteHcl(token);
|
|
671
|
+
if (literal !== undefined)
|
|
672
|
+
return literal;
|
|
673
|
+
if (/^-?\d+(?:\.\d+)?$/.test(token) || token === "true" || token === "false")
|
|
674
|
+
return token;
|
|
675
|
+
const ref = /^(var|local)\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(token);
|
|
676
|
+
if (!ref)
|
|
677
|
+
return undefined;
|
|
678
|
+
return sameFileText(ref[1], ref[2], sameFile);
|
|
679
|
+
}
|
|
680
|
+
function applyPercentS(format, args) {
|
|
681
|
+
let index = 0;
|
|
682
|
+
let text = "";
|
|
683
|
+
for (let cursor = 0; cursor < format.length; cursor += 1) {
|
|
684
|
+
if (format[cursor] !== "%") {
|
|
685
|
+
text += format[cursor];
|
|
686
|
+
continue;
|
|
687
|
+
}
|
|
688
|
+
const verb = format[cursor + 1];
|
|
689
|
+
if (verb === "%") {
|
|
690
|
+
text += "%";
|
|
691
|
+
cursor += 1;
|
|
692
|
+
continue;
|
|
693
|
+
}
|
|
694
|
+
if (verb !== "s" || index >= args.length)
|
|
695
|
+
return undefined;
|
|
696
|
+
text += args[index];
|
|
697
|
+
index += 1;
|
|
698
|
+
cursor += 1;
|
|
699
|
+
}
|
|
700
|
+
return text;
|
|
701
|
+
}
|
|
702
|
+
function resolveFormatCall(value, sameFile) {
|
|
703
|
+
const wrapped = /^\$\{format\(([\s\S]*)\)\}$/.exec(value.trim());
|
|
704
|
+
if (!wrapped)
|
|
705
|
+
return undefined;
|
|
706
|
+
const args = splitCallArguments(wrapped[1]);
|
|
707
|
+
if (!args || args.length === 0)
|
|
708
|
+
return undefined;
|
|
709
|
+
const resolved = [];
|
|
710
|
+
for (const arg of args) {
|
|
711
|
+
const part = resolveFormatArgument(arg, sameFile);
|
|
712
|
+
if (part === undefined)
|
|
713
|
+
return undefined;
|
|
714
|
+
resolved.push(part);
|
|
715
|
+
}
|
|
716
|
+
return applyPercentS(resolved[0], resolved.slice(1));
|
|
717
|
+
}
|
|
718
|
+
function noteResolvedLiteral(literal, path, sameFile, suppressed, referencedSecrets) {
|
|
719
|
+
if (literal.trim() === "")
|
|
720
|
+
return;
|
|
721
|
+
const key = [...path].reverse().find((part) => typeof part === "string");
|
|
722
|
+
if (sameFile.declaredSecrets.has(literal))
|
|
723
|
+
suppressed.add(path.join("."));
|
|
724
|
+
else if (typeof key === "string" && secretAssignmentName(key))
|
|
725
|
+
referencedSecrets.add(path.join("."));
|
|
726
|
+
}
|
|
576
727
|
function sameFileLiteral(value) {
|
|
577
728
|
if (typeof value === "boolean" || typeof value === "number")
|
|
578
729
|
return value;
|
|
@@ -610,19 +761,25 @@ function collectSameFileLiterals(resources) {
|
|
|
610
761
|
function resolveSameFileRefs(value, sameFile, path, suppressed, referencedSecrets) {
|
|
611
762
|
if (typeof value === "string") {
|
|
612
763
|
const match = exactSameFileRef.exec(value.trim());
|
|
613
|
-
if (
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
if (sameFile.declaredSecrets.has(literal))
|
|
621
|
-
suppressed.add(path.join("."));
|
|
622
|
-
else if (typeof key === "string" && secretAssignmentName(String(key)))
|
|
623
|
-
referencedSecrets.add(path.join("."));
|
|
764
|
+
if (match) {
|
|
765
|
+
const literal = (match[1] === "var" ? sameFile.vars : sameFile.locals).get(match[2]);
|
|
766
|
+
if (literal === undefined)
|
|
767
|
+
return value;
|
|
768
|
+
if (typeof literal === "string")
|
|
769
|
+
noteResolvedLiteral(literal, path, sameFile, suppressed, referencedSecrets);
|
|
770
|
+
return literal;
|
|
624
771
|
}
|
|
625
|
-
|
|
772
|
+
const concatenated = resolveInterpolationString(value, sameFile);
|
|
773
|
+
if (concatenated !== undefined) {
|
|
774
|
+
noteResolvedLiteral(concatenated, path, sameFile, suppressed, referencedSecrets);
|
|
775
|
+
return concatenated;
|
|
776
|
+
}
|
|
777
|
+
const formatted = resolveFormatCall(value, sameFile);
|
|
778
|
+
if (formatted !== undefined) {
|
|
779
|
+
noteResolvedLiteral(formatted, path, sameFile, suppressed, referencedSecrets);
|
|
780
|
+
return formatted;
|
|
781
|
+
}
|
|
782
|
+
return value;
|
|
626
783
|
}
|
|
627
784
|
if (Array.isArray(value)) {
|
|
628
785
|
value.forEach((item, index) => {
|
|
@@ -705,6 +862,39 @@ function checkCompose(resource, check) {
|
|
|
705
862
|
service.mem_reservation == null, ["mem_limit"]);
|
|
706
863
|
check("NET001", array(service.ports).some(composePortIsPublic), ["ports"]);
|
|
707
864
|
}
|
|
865
|
+
function statementEffectAllows(body) {
|
|
866
|
+
const effect = body.Effect ?? body.effect;
|
|
867
|
+
if (effect == null || String(effect).trim() === "")
|
|
868
|
+
return true;
|
|
869
|
+
return String(effect).trim().toLowerCase() === "allow";
|
|
870
|
+
}
|
|
871
|
+
function principalAllowsAnyone(value) {
|
|
872
|
+
if (typeof value === "string")
|
|
873
|
+
return value.trim() === "*";
|
|
874
|
+
if (Array.isArray(value))
|
|
875
|
+
return value.some((item) => principalAllowsAnyone(item));
|
|
876
|
+
if (value && typeof value === "object")
|
|
877
|
+
return Object.values(value).some((item) => principalAllowsAnyone(item));
|
|
878
|
+
return false;
|
|
879
|
+
}
|
|
880
|
+
function actionIncludesWildcard(value) {
|
|
881
|
+
return array(value).some((item) => typeof item === "string" && item.includes("*"));
|
|
882
|
+
}
|
|
883
|
+
function checkInlinePolicy(document, check, path) {
|
|
884
|
+
const root = object(document);
|
|
885
|
+
const statements = root.Statement ?? root.statement;
|
|
886
|
+
if (!Array.isArray(statements) && (statements == null || typeof statements !== "object"))
|
|
887
|
+
return;
|
|
888
|
+
for (const statement of array(statements)) {
|
|
889
|
+
const body = object(statement);
|
|
890
|
+
if (!statementEffectAllows(body))
|
|
891
|
+
continue;
|
|
892
|
+
check("IAM001", actionIncludesWildcard(body.Action ?? body.action ?? body.Actions ?? body.actions), path);
|
|
893
|
+
const principal = body.Principal ?? body.principal ?? body.Principals ?? body.principals;
|
|
894
|
+
if (principal !== undefined)
|
|
895
|
+
check("IAM002", principalAllowsAnyone(principal), path);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
708
898
|
function checkCloud(resource, resources, format, check) {
|
|
709
899
|
const tf = format === "terraform";
|
|
710
900
|
const pulumi = format === "pulumi";
|
|
@@ -727,31 +917,30 @@ function checkCloud(resource, resources, format, check) {
|
|
|
727
917
|
isIngress) {
|
|
728
918
|
check("NET001", array(child).some((cidr) => cidr === "0.0.0.0/0" || cidr === "::/0"), [...prefix, ...at]);
|
|
729
919
|
}
|
|
730
|
-
if (/^(?:Action|
|
|
731
|
-
(
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
].some((v) => typeof v === "string" && v.includes("*")), [...prefix, ...at]);
|
|
920
|
+
if (/^(?:Action|actions)$/.test(key) && statementEffectAllows(parent)) {
|
|
921
|
+
check("IAM001", actionIncludesWildcard(child), [...prefix, ...at]);
|
|
922
|
+
}
|
|
923
|
+
if (/^(?:Principal|principals)$/.test(key) && statementEffectAllows(parent)) {
|
|
924
|
+
check("IAM002", principalAllowsAnyone(child), [...prefix, ...at]);
|
|
925
|
+
}
|
|
926
|
+
if (typeof child === "string") {
|
|
927
|
+
const encoded = child.trim().startsWith("${jsonencode(")
|
|
928
|
+
? terraformJsonencodeValue(child)
|
|
929
|
+
: undefined;
|
|
930
|
+
let parsed;
|
|
931
|
+
if (encoded === undefined &&
|
|
932
|
+
/^(?:policy|assume_role_policy|PolicyDocument)$/.test(key) &&
|
|
933
|
+
child.trimStart().startsWith("{")) {
|
|
934
|
+
try {
|
|
935
|
+
parsed = JSON.parse(child);
|
|
936
|
+
}
|
|
937
|
+
catch {
|
|
938
|
+
parsed = undefined;
|
|
750
939
|
}
|
|
751
940
|
}
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
941
|
+
const document = encoded ?? parsed;
|
|
942
|
+
if (document !== undefined)
|
|
943
|
+
checkInlinePolicy(document, check, [...prefix, ...at]);
|
|
755
944
|
}
|
|
756
945
|
if (/^(?:instance_type|machine_type|vm_size|InstanceType|DBInstanceClass|instance_class)$/.test(key))
|
|
757
946
|
check("COST001", typeof child === "string", [...prefix, ...at]);
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.6.
|
|
1
|
+
export declare const VERSION = "0.6.15";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.6.
|
|
1
|
+
export const VERSION = "0.6.15";
|