@skaleagents/swarm 0.6.15 → 0.6.16
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 +9 -2
- package/dist/iac/parse.js +244 -22
- package/dist/iac/rules.d.ts +7 -1
- package/dist/iac/rules.js +142 -18
- package/dist/iac/scan.d.ts +1 -1
- package/dist/iac/scan.js +129 -18
- package/dist/server.js +5 -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" | "gitlab" | "azure-pipelines" | "cloudbuild" | "tfvars" | "serverless" | "iam";
|
|
1
|
+
export type IacFormat = "terraform" | "cloudformation" | "kubernetes" | "pulumi" | "compose" | "dockerfile" | "github" | "helm" | "ansible" | "bicep" | "arm" | "gitlab" | "azure-pipelines" | "cloudbuild" | "tfvars" | "serverless" | "iam" | "package";
|
|
2
2
|
export type Path = (string | number)[];
|
|
3
3
|
export type Location = {
|
|
4
4
|
line: number;
|
|
@@ -32,7 +32,14 @@ export declare function looksLikeTerraformHcl(content: string): boolean;
|
|
|
32
32
|
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
|
-
export
|
|
35
|
+
export type CloudFormationTables = {
|
|
36
|
+
defaults: Map<string, string | boolean | number>;
|
|
37
|
+
secretNames: Set<string>;
|
|
38
|
+
mappings: Map<string, Record<string, Record<string, string | number | boolean>>>;
|
|
39
|
+
};
|
|
40
|
+
export declare function parseIac(content: string, requested?: IacFormat | "auto", shared?: CloudFormationTables): ParsedIac;
|
|
41
|
+
export declare function readCloudFormationTables(content: string): CloudFormationTables;
|
|
36
42
|
export declare function terraformJsonencodeValue(expression: string): unknown | undefined;
|
|
37
43
|
export declare function looksLikeServerless(content: string): boolean;
|
|
44
|
+
export declare function looksLikePackage(content: string): boolean;
|
|
38
45
|
export declare function looksLikeIamPolicy(content: string): boolean;
|
package/dist/iac/parse.js
CHANGED
|
@@ -32,6 +32,8 @@ export function looksLikeIac(content) {
|
|
|
32
32
|
return true;
|
|
33
33
|
if (looksLikeIamPolicy(content))
|
|
34
34
|
return true;
|
|
35
|
+
if (looksLikePackage(content))
|
|
36
|
+
return true;
|
|
35
37
|
}
|
|
36
38
|
catch {
|
|
37
39
|
/* Other format detection still applies to incomplete input. */
|
|
@@ -139,10 +141,12 @@ export function looksLikePulumiYaml(content) {
|
|
|
139
141
|
return (/^\s*resources\s*:/m.test(content) &&
|
|
140
142
|
/^\s+type\s*:\s*[A-Za-z0-9_.-]+:\S+/m.test(content));
|
|
141
143
|
}
|
|
142
|
-
const terraformExpressionWarning = "
|
|
143
|
-
export function parseIac(content, requested = "auto") {
|
|
144
|
+
const terraformExpressionWarning = "Variable and local literals from the submitted files are resolved. A string of those references, format of known literals, and join of known literals are resolved. Other functions and expressions are not evaluated.";
|
|
145
|
+
export function parseIac(content, requested = "auto", shared) {
|
|
144
146
|
if (!content.trim())
|
|
145
147
|
throw new ScanInputError("Content must not be empty or whitespace.");
|
|
148
|
+
if (requested === "package" || (requested === "auto" && looksLikePackage(content)))
|
|
149
|
+
return parsePackage(content);
|
|
146
150
|
// Detect these before HCL or a failing YAML parse so a Dockerfile is not classified as Terraform.
|
|
147
151
|
if (requested === "dockerfile" ||
|
|
148
152
|
((requested === "auto" || requested === "terraform") &&
|
|
@@ -412,12 +416,28 @@ export function parseIac(content, requested = "auto") {
|
|
|
412
416
|
Array.isArray(root.Resources) ||
|
|
413
417
|
typeof root.Resources !== "object")
|
|
414
418
|
throw new ScanInputError("CloudFormation requires a Resources mapping.");
|
|
415
|
-
const
|
|
419
|
+
const localParameters = cloudFormationParameterLiterals(root);
|
|
420
|
+
const localMappings = cloudFormationMappings(root);
|
|
421
|
+
const parameters = {
|
|
422
|
+
defaults: new Map(shared?.defaults ?? localParameters.defaults),
|
|
423
|
+
secretNames: new Set(shared?.secretNames ?? localParameters.secretNames),
|
|
424
|
+
};
|
|
425
|
+
const mappings = new Map(shared?.mappings ?? localMappings);
|
|
426
|
+
if (shared) {
|
|
427
|
+
for (const [name, value] of localParameters.defaults)
|
|
428
|
+
if (!parameters.defaults.has(name))
|
|
429
|
+
parameters.defaults.set(name, value);
|
|
430
|
+
for (const name of localParameters.secretNames)
|
|
431
|
+
parameters.secretNames.add(name);
|
|
432
|
+
for (const [name, value] of localMappings)
|
|
433
|
+
if (!mappings.has(name))
|
|
434
|
+
mappings.set(name, value);
|
|
435
|
+
}
|
|
416
436
|
for (const [name, raw] of Object.entries(object(root.Resources))) {
|
|
417
437
|
const value = object(raw);
|
|
418
438
|
if (typeof value.Type !== "string")
|
|
419
439
|
throw new ScanInputError("Each CloudFormation resource requires a Type.");
|
|
420
|
-
add(name, value.Type, resolveCloudFormationRefs(value, parameters), ["Resources", name]);
|
|
440
|
+
add(name, value.Type, resolveCloudFormationRefs(value, parameters, mappings), ["Resources", name]);
|
|
421
441
|
}
|
|
422
442
|
}
|
|
423
443
|
else if (format === "terraform") {
|
|
@@ -465,12 +485,12 @@ export function parseIac(content, requested = "auto") {
|
|
|
465
485
|
}
|
|
466
486
|
}
|
|
467
487
|
if (!format)
|
|
468
|
-
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,
|
|
488
|
+
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, a standalone IAM policy, or a package.json.");
|
|
469
489
|
if (!resources.length)
|
|
470
490
|
warnings.push("No resources found in the submitted document.");
|
|
471
491
|
if (format === "cloudformation" &&
|
|
472
492
|
/(?:!(?:Ref|Sub|GetAtt|If)\b|"(?:Ref|Fn::\w+)"\s*:|\bRef\s*:)/.test(content))
|
|
473
|
-
warnings.push("Same-file parameter defaults are resolved. A simple Fn::Sub of
|
|
493
|
+
warnings.push("Same-file and cross-file parameter defaults are resolved. A simple Fn::Sub, Fn::Join, and Fn::FindInMap of known literals are resolved. Other functions are not evaluated.");
|
|
474
494
|
else if (/(?:!(?:Ref|Sub|GetAtt|If)\b|"(?:Ref|Fn::\w+)"\s*:|\$\{)/.test(content))
|
|
475
495
|
warnings.push("Intrinsic functions and expressions are not evaluated. Only literal configuration is checked.");
|
|
476
496
|
return { format, resources, warnings };
|
|
@@ -1413,6 +1433,58 @@ function parameterNameIsSecret(name) {
|
|
|
1413
1433
|
/(?:^|_)(?:api_?key|auth_token|access_key|private_key)$/i.test(normalized) ||
|
|
1414
1434
|
/(?:password|passwd|secret|api[_-]?key|auth[_-]?token|access[_-]?key|private[_-]?key|token)$/i.test(name));
|
|
1415
1435
|
}
|
|
1436
|
+
function cloudFormationMappings(root) {
|
|
1437
|
+
const mappings = new Map();
|
|
1438
|
+
for (const [mapName, top] of Object.entries(object(root.Mappings))) {
|
|
1439
|
+
const levels = {};
|
|
1440
|
+
for (const [topKey, second] of Object.entries(object(top))) {
|
|
1441
|
+
const entries = {};
|
|
1442
|
+
for (const [secondKey, raw] of Object.entries(object(second))) {
|
|
1443
|
+
if (typeof raw === "string" || typeof raw === "number" || typeof raw === "boolean")
|
|
1444
|
+
entries[secondKey] = raw;
|
|
1445
|
+
}
|
|
1446
|
+
levels[topKey] = entries;
|
|
1447
|
+
}
|
|
1448
|
+
mappings.set(mapName, levels);
|
|
1449
|
+
}
|
|
1450
|
+
return mappings;
|
|
1451
|
+
}
|
|
1452
|
+
export function readCloudFormationTables(content) {
|
|
1453
|
+
const empty = () => ({
|
|
1454
|
+
defaults: new Map(),
|
|
1455
|
+
secretNames: new Set(),
|
|
1456
|
+
mappings: new Map(),
|
|
1457
|
+
});
|
|
1458
|
+
const trimmed = content.trim();
|
|
1459
|
+
if (!trimmed)
|
|
1460
|
+
return empty();
|
|
1461
|
+
let root;
|
|
1462
|
+
try {
|
|
1463
|
+
if (trimmed.startsWith("{"))
|
|
1464
|
+
root = object(JSON.parse(trimmed));
|
|
1465
|
+
else {
|
|
1466
|
+
const docs = parseAllDocuments(trimmed, { prettyErrors: false });
|
|
1467
|
+
const doc = docs[0];
|
|
1468
|
+
if (!doc || doc.errors.length)
|
|
1469
|
+
return empty();
|
|
1470
|
+
root = object(doc.toJS({ maxAliasCount: 0 }));
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
catch {
|
|
1474
|
+
return empty();
|
|
1475
|
+
}
|
|
1476
|
+
if (!root.Parameters &&
|
|
1477
|
+
!root.Mappings &&
|
|
1478
|
+
!root.Resources &&
|
|
1479
|
+
typeof root.AWSTemplateFormatVersion !== "string")
|
|
1480
|
+
return empty();
|
|
1481
|
+
const parameters = cloudFormationParameterLiterals(root);
|
|
1482
|
+
return {
|
|
1483
|
+
defaults: parameters.defaults,
|
|
1484
|
+
secretNames: parameters.secretNames,
|
|
1485
|
+
mappings: cloudFormationMappings(root),
|
|
1486
|
+
};
|
|
1487
|
+
}
|
|
1416
1488
|
function cloudFormationParameterLiterals(root) {
|
|
1417
1489
|
const defaults = new Map();
|
|
1418
1490
|
const secretNames = new Set();
|
|
@@ -1459,6 +1531,7 @@ function cloudFormationSubTemplate(value) {
|
|
|
1459
1531
|
function substituteKnownParameters(template, parameters) {
|
|
1460
1532
|
let unresolved = false;
|
|
1461
1533
|
let secret = false;
|
|
1534
|
+
const names = [];
|
|
1462
1535
|
const text = template.replace(/\$\{(!?)([^}]*)\}/g, (full, literalMark, rawName) => {
|
|
1463
1536
|
if (literalMark === "!")
|
|
1464
1537
|
return `\${${rawName}}`;
|
|
@@ -1467,30 +1540,70 @@ function substituteKnownParameters(template, parameters) {
|
|
|
1467
1540
|
unresolved = true;
|
|
1468
1541
|
return full;
|
|
1469
1542
|
}
|
|
1543
|
+
names.push(name);
|
|
1470
1544
|
if (parameters.secretNames.has(name))
|
|
1471
1545
|
secret = true;
|
|
1472
1546
|
return String(parameters.defaults.get(name));
|
|
1473
1547
|
});
|
|
1474
1548
|
if (unresolved)
|
|
1475
1549
|
return undefined;
|
|
1476
|
-
return { text, secret };
|
|
1550
|
+
return { text, secret, names };
|
|
1477
1551
|
}
|
|
1478
|
-
function
|
|
1552
|
+
function cloudFormationJoin(value) {
|
|
1553
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
1554
|
+
return undefined;
|
|
1555
|
+
const entries = Object.entries(value).filter(([key]) => key !== "__referencedSecrets" && key !== "__valueOrigins");
|
|
1556
|
+
if (entries.length !== 1)
|
|
1557
|
+
return undefined;
|
|
1558
|
+
const [key, body] = entries[0];
|
|
1559
|
+
if (key !== "Fn::Join" || !Array.isArray(body) || body.length !== 2 || !Array.isArray(body[1]))
|
|
1560
|
+
return undefined;
|
|
1561
|
+
return [body[0], body[1]];
|
|
1562
|
+
}
|
|
1563
|
+
function cloudFormationFindInMap(value) {
|
|
1564
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
1565
|
+
return undefined;
|
|
1566
|
+
const entries = Object.entries(value).filter(([key]) => key !== "__referencedSecrets" && key !== "__valueOrigins");
|
|
1567
|
+
if (entries.length !== 1)
|
|
1568
|
+
return undefined;
|
|
1569
|
+
const [key, body] = entries[0];
|
|
1570
|
+
if (key !== "Fn::FindInMap" || !Array.isArray(body) || body.length !== 3)
|
|
1571
|
+
return undefined;
|
|
1572
|
+
return [body[0], body[1], body[2]];
|
|
1573
|
+
}
|
|
1574
|
+
function knownLiteral(value) {
|
|
1575
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean")
|
|
1576
|
+
return String(value);
|
|
1577
|
+
return undefined;
|
|
1578
|
+
}
|
|
1579
|
+
function recordSingleOrigin(origins, path, names) {
|
|
1580
|
+
const unique = [...new Set(names)];
|
|
1581
|
+
if (unique.length === 1)
|
|
1582
|
+
origins.set(path.join("."), unique[0]);
|
|
1583
|
+
}
|
|
1584
|
+
function resolveCloudFormationRefs(value, parameters, mappings) {
|
|
1479
1585
|
const secretPaths = [];
|
|
1480
|
-
const
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
}
|
|
1586
|
+
const origins = new Map();
|
|
1587
|
+
const resolved = resolveCloudFormationValue(value, parameters, mappings, [], secretPaths, origins);
|
|
1588
|
+
if (resolved && typeof resolved === "object" && !Array.isArray(resolved)) {
|
|
1589
|
+
if (secretPaths.length > 0) {
|
|
1590
|
+
Object.defineProperty(resolved, "__referencedSecrets", {
|
|
1591
|
+
enumerable: false,
|
|
1592
|
+
configurable: true,
|
|
1593
|
+
value: secretPaths,
|
|
1594
|
+
});
|
|
1595
|
+
}
|
|
1596
|
+
if (origins.size > 0) {
|
|
1597
|
+
Object.defineProperty(resolved, "__valueOrigins", {
|
|
1598
|
+
enumerable: false,
|
|
1599
|
+
configurable: true,
|
|
1600
|
+
value: origins,
|
|
1601
|
+
});
|
|
1602
|
+
}
|
|
1490
1603
|
}
|
|
1491
1604
|
return object(resolved);
|
|
1492
1605
|
}
|
|
1493
|
-
function resolveCloudFormationValue(value, parameters, path, secretPaths) {
|
|
1606
|
+
function resolveCloudFormationValue(value, parameters, mappings, path, secretPaths, origins) {
|
|
1494
1607
|
const ref = cloudFormationRefName(value);
|
|
1495
1608
|
if (ref) {
|
|
1496
1609
|
if (!parameters.defaults.has(ref))
|
|
@@ -1500,6 +1613,7 @@ function resolveCloudFormationValue(value, parameters, path, secretPaths) {
|
|
|
1500
1613
|
typeof literal === "string" &&
|
|
1501
1614
|
literal.trim() !== "")
|
|
1502
1615
|
secretPaths.push(path);
|
|
1616
|
+
origins.set(path.join("."), `parameter ${ref}`);
|
|
1503
1617
|
return literal;
|
|
1504
1618
|
}
|
|
1505
1619
|
const subTemplate = cloudFormationSubTemplate(value);
|
|
@@ -1509,18 +1623,46 @@ function resolveCloudFormationValue(value, parameters, path, secretPaths) {
|
|
|
1509
1623
|
return value;
|
|
1510
1624
|
if (substituted.secret)
|
|
1511
1625
|
secretPaths.push(path);
|
|
1626
|
+
recordSingleOrigin(origins, path, substituted.names.map((name) => `parameter ${name}`));
|
|
1512
1627
|
return substituted.text;
|
|
1513
1628
|
}
|
|
1629
|
+
const join = cloudFormationJoin(value);
|
|
1630
|
+
if (join) {
|
|
1631
|
+
const [separator, list] = join;
|
|
1632
|
+
const resolvedSeparator = resolveCloudFormationValue(separator, parameters, mappings, [...path, 0], secretPaths, origins);
|
|
1633
|
+
const separatorText = knownLiteral(resolvedSeparator);
|
|
1634
|
+
if (separatorText === undefined)
|
|
1635
|
+
return value;
|
|
1636
|
+
const parts = [];
|
|
1637
|
+
for (const [index, item] of list.entries()) {
|
|
1638
|
+
const resolved = resolveCloudFormationValue(item, parameters, mappings, [...path, 1, index], secretPaths, origins);
|
|
1639
|
+
const text = knownLiteral(resolved);
|
|
1640
|
+
if (text === undefined)
|
|
1641
|
+
return value;
|
|
1642
|
+
parts.push(text);
|
|
1643
|
+
}
|
|
1644
|
+
return parts.join(separatorText);
|
|
1645
|
+
}
|
|
1646
|
+
const find = cloudFormationFindInMap(value);
|
|
1647
|
+
if (find) {
|
|
1648
|
+
const [mapName, topKey, secondKey] = find.map((key) => knownLiteral(key));
|
|
1649
|
+
if (!mapName || !topKey || !secondKey)
|
|
1650
|
+
return value;
|
|
1651
|
+
const mapped = mappings.get(mapName)?.[topKey]?.[secondKey];
|
|
1652
|
+
if (mapped === undefined)
|
|
1653
|
+
return value;
|
|
1654
|
+
return mapped;
|
|
1655
|
+
}
|
|
1514
1656
|
if (cloudFormationIntrinsic(value))
|
|
1515
1657
|
return value;
|
|
1516
1658
|
if (Array.isArray(value))
|
|
1517
|
-
return value.map((item, index) => resolveCloudFormationValue(item, parameters, [...path, index], secretPaths));
|
|
1659
|
+
return value.map((item, index) => resolveCloudFormationValue(item, parameters, mappings, [...path, index], secretPaths, origins));
|
|
1518
1660
|
if (value && typeof value === "object") {
|
|
1519
1661
|
const out = {};
|
|
1520
1662
|
for (const [key, child] of Object.entries(value)) {
|
|
1521
|
-
if (key === "__referencedSecrets")
|
|
1663
|
+
if (key === "__referencedSecrets" || key === "__valueOrigins")
|
|
1522
1664
|
continue;
|
|
1523
|
-
out[key] = resolveCloudFormationValue(child, parameters, [...path, key], secretPaths);
|
|
1665
|
+
out[key] = resolveCloudFormationValue(child, parameters, mappings, [...path, key], secretPaths, origins);
|
|
1524
1666
|
}
|
|
1525
1667
|
return out;
|
|
1526
1668
|
}
|
|
@@ -1558,6 +1700,86 @@ export function looksLikeServerless(content) {
|
|
|
1558
1700
|
return false;
|
|
1559
1701
|
return topLevelKey(content, "functions") || topLevelKey(content, "plugins");
|
|
1560
1702
|
}
|
|
1703
|
+
function plainObject(value) {
|
|
1704
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1705
|
+
}
|
|
1706
|
+
export function looksLikePackage(content) {
|
|
1707
|
+
const trimmed = content.trim();
|
|
1708
|
+
if (!trimmed.startsWith("{"))
|
|
1709
|
+
return false;
|
|
1710
|
+
let value;
|
|
1711
|
+
try {
|
|
1712
|
+
value = JSON.parse(trimmed);
|
|
1713
|
+
}
|
|
1714
|
+
catch {
|
|
1715
|
+
return false;
|
|
1716
|
+
}
|
|
1717
|
+
const root = object(value);
|
|
1718
|
+
if (typeof root.name !== "string" || root.name.trim() === "")
|
|
1719
|
+
return false;
|
|
1720
|
+
if ("resource" in root || "Resources" in root || typeof root.AWSTemplateFormatVersion === "string")
|
|
1721
|
+
return false;
|
|
1722
|
+
if (looksLikeIamPolicy(trimmed))
|
|
1723
|
+
return false;
|
|
1724
|
+
return (plainObject(root.dependencies) ||
|
|
1725
|
+
plainObject(root.devDependencies) ||
|
|
1726
|
+
plainObject(root.scripts));
|
|
1727
|
+
}
|
|
1728
|
+
function scriptPipesDownload(command) {
|
|
1729
|
+
return /\b(?:curl|wget)\b[\s\S]*\|\s*(?:ba)?sh\b/.test(command);
|
|
1730
|
+
}
|
|
1731
|
+
function jsonKeyLine(content, key) {
|
|
1732
|
+
const match = new RegExp(`"${escapeRegex(key)}"\\s*:`).exec(content);
|
|
1733
|
+
if (!match)
|
|
1734
|
+
return 1;
|
|
1735
|
+
return content.slice(0, match.index).split("\n").length;
|
|
1736
|
+
}
|
|
1737
|
+
function parsePackage(content) {
|
|
1738
|
+
if (!looksLikePackage(content))
|
|
1739
|
+
throw new ScanInputError("A package.json requires a top-level name plus dependencies, devDependencies, or scripts.");
|
|
1740
|
+
const root = object(JSON.parse(content));
|
|
1741
|
+
const resources = [];
|
|
1742
|
+
resources.push({
|
|
1743
|
+
id: String(root.name),
|
|
1744
|
+
type: "package:manifest",
|
|
1745
|
+
value: { name: root.name },
|
|
1746
|
+
locate: () => ({ line: jsonKeyLine(content, "name"), column: 1, path: "name" }),
|
|
1747
|
+
});
|
|
1748
|
+
const scripts = object(root.scripts);
|
|
1749
|
+
for (const name of ["preinstall", "install", "postinstall"]) {
|
|
1750
|
+
if (typeof scripts[name] !== "string")
|
|
1751
|
+
continue;
|
|
1752
|
+
resources.push({
|
|
1753
|
+
id: `scripts.${name}`,
|
|
1754
|
+
type: "package:script",
|
|
1755
|
+
value: { name, command: scripts[name], pipes: scriptPipesDownload(scripts[name]) },
|
|
1756
|
+
locate: () => ({
|
|
1757
|
+
line: jsonKeyLine(content, name),
|
|
1758
|
+
column: 1,
|
|
1759
|
+
path: `scripts.${name}`,
|
|
1760
|
+
}),
|
|
1761
|
+
});
|
|
1762
|
+
}
|
|
1763
|
+
for (const field of ["dependencies", "devDependencies", "optionalDependencies"]) {
|
|
1764
|
+
if (!plainObject(root[field]))
|
|
1765
|
+
continue;
|
|
1766
|
+
for (const [dep, range] of Object.entries(root[field])) {
|
|
1767
|
+
if (range !== "*" && range !== "latest")
|
|
1768
|
+
continue;
|
|
1769
|
+
resources.push({
|
|
1770
|
+
id: `${field}.${dep}`,
|
|
1771
|
+
type: "package:dependency",
|
|
1772
|
+
value: { field, name: dep, range },
|
|
1773
|
+
locate: () => ({
|
|
1774
|
+
line: jsonKeyLine(content, dep),
|
|
1775
|
+
column: 1,
|
|
1776
|
+
path: `${field}.${dep}`,
|
|
1777
|
+
}),
|
|
1778
|
+
});
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
return { format: "package", resources, warnings: [] };
|
|
1782
|
+
}
|
|
1561
1783
|
export function looksLikeIamPolicy(content) {
|
|
1562
1784
|
const trimmed = content.trim();
|
|
1563
1785
|
if (!trimmed.startsWith("{"))
|
package/dist/iac/rules.d.ts
CHANGED
|
@@ -9,7 +9,13 @@ type Rule = {
|
|
|
9
9
|
remediation: string;
|
|
10
10
|
};
|
|
11
11
|
export declare const rules: Record<string, Rule>;
|
|
12
|
-
export
|
|
12
|
+
export type TerraformLiterals = {
|
|
13
|
+
vars: Map<string, string | boolean | number>;
|
|
14
|
+
locals: Map<string, string | boolean | number>;
|
|
15
|
+
declaredSecrets: Set<string>;
|
|
16
|
+
};
|
|
17
|
+
export declare function terraformLiterals(resources: Resource[]): TerraformLiterals;
|
|
18
|
+
export declare function resourceFindings(resources: Resource[], format: string, shared?: TerraformLiterals): {
|
|
13
19
|
findings: Finding[];
|
|
14
20
|
checked: Set<string>;
|
|
15
21
|
};
|
package/dist/iac/rules.js
CHANGED
|
@@ -372,13 +372,31 @@ export const rules = {
|
|
|
372
372
|
detail: "A Serverless Framework environment entry assigns a non-empty literal to a secret-like name.",
|
|
373
373
|
remediation: "Reference the value with a Serverless variable such as ${env:TOKEN}. Do not commit a literal secret.",
|
|
374
374
|
},
|
|
375
|
+
PKG001: {
|
|
376
|
+
category: "security",
|
|
377
|
+
severity: "high",
|
|
378
|
+
title: "Package script pipes a download to a shell",
|
|
379
|
+
detail: "A package install script downloads with curl or wget and pipes the result to a shell.",
|
|
380
|
+
remediation: "Run a reviewed local script instead of piping a remote download to a shell.",
|
|
381
|
+
},
|
|
382
|
+
PKG002: {
|
|
383
|
+
category: "security",
|
|
384
|
+
severity: "medium",
|
|
385
|
+
title: "Dependency range is floating",
|
|
386
|
+
detail: "A dependency range is * or latest.",
|
|
387
|
+
remediation: "Pin the dependency to a reviewed version.",
|
|
388
|
+
},
|
|
375
389
|
};
|
|
376
|
-
export function
|
|
390
|
+
export function terraformLiterals(resources) {
|
|
391
|
+
return collectSameFileLiterals(resources);
|
|
392
|
+
}
|
|
393
|
+
export function resourceFindings(resources, format, shared) {
|
|
377
394
|
const findings = [];
|
|
378
395
|
const checked = new Set();
|
|
379
|
-
const sameFile = collectSameFileLiterals(resources);
|
|
396
|
+
const sameFile = shared ?? collectSameFileLiterals(resources);
|
|
380
397
|
const suppressedSecrets = new WeakMap();
|
|
381
398
|
const moduleSecrets = new WeakMap();
|
|
399
|
+
const valueOrigins = new WeakMap();
|
|
382
400
|
if (format === "terraform") {
|
|
383
401
|
for (const resource of resources) {
|
|
384
402
|
if (resource.type === "terraform:variable" ||
|
|
@@ -387,18 +405,22 @@ export function resourceFindings(resources, format) {
|
|
|
387
405
|
continue;
|
|
388
406
|
const suppressed = new Set();
|
|
389
407
|
const referenced = new Set();
|
|
390
|
-
|
|
408
|
+
const origins = new Map();
|
|
409
|
+
resolveSameFileRefs(resource.value, sameFile, [], suppressed, referenced, origins);
|
|
391
410
|
suppressedSecrets.set(resource, suppressed);
|
|
392
411
|
moduleSecrets.set(resource, referenced);
|
|
412
|
+
valueOrigins.set(resource, origins);
|
|
393
413
|
}
|
|
394
414
|
}
|
|
395
415
|
for (const resource of resources) {
|
|
416
|
+
const origins = valueOrigins.get(resource) ?? cloudFormationOrigins(resource);
|
|
396
417
|
const check = (id, condition, path = []) => {
|
|
397
418
|
checked.add(id);
|
|
398
419
|
if (condition)
|
|
399
420
|
findings.push({
|
|
400
421
|
ruleId: id,
|
|
401
422
|
...rules[id],
|
|
423
|
+
detail: `${rules[id].detail}${originSentence(origins, path)}`,
|
|
402
424
|
resource: resource.id,
|
|
403
425
|
location: resource.locate(path),
|
|
404
426
|
});
|
|
@@ -438,6 +460,10 @@ export function resourceFindings(resources, format) {
|
|
|
438
460
|
check("NET001", resource.value.openSourceIp === true, ["Condition"]);
|
|
439
461
|
continue;
|
|
440
462
|
}
|
|
463
|
+
if (format === "package") {
|
|
464
|
+
checkPackage(resource, check);
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
441
467
|
if (resource.type === "terraform:local") {
|
|
442
468
|
checkTerraformLiterals(resource.value, "local", check, []);
|
|
443
469
|
continue;
|
|
@@ -592,6 +618,7 @@ function resolveInterpolationString(value, sameFile) {
|
|
|
592
618
|
const trimmed = value.trim();
|
|
593
619
|
if (!interpolationOnly.test(trimmed))
|
|
594
620
|
return undefined;
|
|
621
|
+
const sources = [];
|
|
595
622
|
let count = 0;
|
|
596
623
|
let failed = false;
|
|
597
624
|
const text = trimmed.replace(interpolationPiece, (_full, kind, name) => {
|
|
@@ -601,11 +628,12 @@ function resolveInterpolationString(value, sameFile) {
|
|
|
601
628
|
failed = true;
|
|
602
629
|
return "";
|
|
603
630
|
}
|
|
631
|
+
sources.push(`${kind}.${name}`);
|
|
604
632
|
return part;
|
|
605
633
|
});
|
|
606
634
|
if (failed || count < 2)
|
|
607
635
|
return undefined;
|
|
608
|
-
return text;
|
|
636
|
+
return { text, sources };
|
|
609
637
|
}
|
|
610
638
|
function splitCallArguments(source) {
|
|
611
639
|
const args = [];
|
|
@@ -668,14 +696,27 @@ function unquoteHcl(token) {
|
|
|
668
696
|
}
|
|
669
697
|
function resolveFormatArgument(token, sameFile) {
|
|
670
698
|
const literal = unquoteHcl(token);
|
|
671
|
-
if (literal !== undefined)
|
|
672
|
-
|
|
699
|
+
if (literal !== undefined) {
|
|
700
|
+
const ref = exactSameFileRef.exec(literal.trim());
|
|
701
|
+
if (!ref) {
|
|
702
|
+
if (literal.includes("${"))
|
|
703
|
+
return undefined;
|
|
704
|
+
return { text: literal };
|
|
705
|
+
}
|
|
706
|
+
const text = sameFileText(ref[1], ref[2], sameFile);
|
|
707
|
+
if (text === undefined)
|
|
708
|
+
return undefined;
|
|
709
|
+
return { text, source: `${ref[1]}.${ref[2]}` };
|
|
710
|
+
}
|
|
673
711
|
if (/^-?\d+(?:\.\d+)?$/.test(token) || token === "true" || token === "false")
|
|
674
|
-
return token;
|
|
712
|
+
return { text: token };
|
|
675
713
|
const ref = /^(var|local)\.([A-Za-z_][A-Za-z0-9_]*)$/.exec(token);
|
|
676
714
|
if (!ref)
|
|
677
715
|
return undefined;
|
|
678
|
-
|
|
716
|
+
const text = sameFileText(ref[1], ref[2], sameFile);
|
|
717
|
+
if (text === undefined)
|
|
718
|
+
return undefined;
|
|
719
|
+
return { text, source: `${ref[1]}.${ref[2]}` };
|
|
679
720
|
}
|
|
680
721
|
function applyPercentS(format, args) {
|
|
681
722
|
let index = 0;
|
|
@@ -707,13 +748,59 @@ function resolveFormatCall(value, sameFile) {
|
|
|
707
748
|
if (!args || args.length === 0)
|
|
708
749
|
return undefined;
|
|
709
750
|
const resolved = [];
|
|
751
|
+
const sources = [];
|
|
710
752
|
for (const arg of args) {
|
|
711
753
|
const part = resolveFormatArgument(arg, sameFile);
|
|
712
754
|
if (part === undefined)
|
|
713
755
|
return undefined;
|
|
714
|
-
resolved.push(part);
|
|
756
|
+
resolved.push(part.text);
|
|
757
|
+
if (part.source)
|
|
758
|
+
sources.push(part.source);
|
|
715
759
|
}
|
|
716
|
-
|
|
760
|
+
const text = applyPercentS(resolved[0], resolved.slice(1));
|
|
761
|
+
if (text === undefined)
|
|
762
|
+
return undefined;
|
|
763
|
+
return { text, sources };
|
|
764
|
+
}
|
|
765
|
+
function parseHclList(token) {
|
|
766
|
+
const trimmed = token.trim();
|
|
767
|
+
if (!trimmed.startsWith("[") || !trimmed.endsWith("]"))
|
|
768
|
+
return undefined;
|
|
769
|
+
return splitCallArguments(trimmed.slice(1, -1)) ?? [];
|
|
770
|
+
}
|
|
771
|
+
function resolveJoinCall(value, sameFile, depth = 0) {
|
|
772
|
+
if (depth > 8)
|
|
773
|
+
return undefined;
|
|
774
|
+
const wrapped = /^\$\{join\(([\s\S]*)\)\}$/.exec(value.trim());
|
|
775
|
+
const bare = /^join\(([\s\S]*)\)$/.exec(value.trim());
|
|
776
|
+
const source = wrapped?.[1] ?? bare?.[1];
|
|
777
|
+
if (source === undefined)
|
|
778
|
+
return undefined;
|
|
779
|
+
const args = splitCallArguments(source);
|
|
780
|
+
if (!args || args.length !== 2)
|
|
781
|
+
return undefined;
|
|
782
|
+
const separator = resolveJoinPiece(args[0], sameFile, depth);
|
|
783
|
+
const list = parseHclList(args[1]);
|
|
784
|
+
if (!separator || !list)
|
|
785
|
+
return undefined;
|
|
786
|
+
const parts = [];
|
|
787
|
+
const sources = [...separator.sources];
|
|
788
|
+
for (const element of list) {
|
|
789
|
+
const resolved = resolveJoinPiece(element, sameFile, depth);
|
|
790
|
+
if (!resolved)
|
|
791
|
+
return undefined;
|
|
792
|
+
parts.push(resolved.text);
|
|
793
|
+
sources.push(...resolved.sources);
|
|
794
|
+
}
|
|
795
|
+
return { text: parts.join(separator.text), sources };
|
|
796
|
+
}
|
|
797
|
+
function resolveJoinPiece(token, sameFile, depth) {
|
|
798
|
+
if (/^join\(/.test(token.trim()))
|
|
799
|
+
return resolveJoinCall(`\${${token.trim()}}`, sameFile, depth + 1);
|
|
800
|
+
const resolved = resolveFormatArgument(token, sameFile);
|
|
801
|
+
if (!resolved)
|
|
802
|
+
return undefined;
|
|
803
|
+
return { text: resolved.text, sources: resolved.source ? [resolved.source] : [] };
|
|
717
804
|
}
|
|
718
805
|
function noteResolvedLiteral(literal, path, sameFile, suppressed, referencedSecrets) {
|
|
719
806
|
if (literal.trim() === "")
|
|
@@ -758,7 +845,12 @@ function collectSameFileLiterals(resources) {
|
|
|
758
845
|
}
|
|
759
846
|
return { vars, locals, declaredSecrets };
|
|
760
847
|
}
|
|
761
|
-
function
|
|
848
|
+
function recordResolvedSources(origins, path, sources) {
|
|
849
|
+
const unique = [...new Set(sources)];
|
|
850
|
+
if (unique.length === 1)
|
|
851
|
+
origins.set(path.join("."), unique[0]);
|
|
852
|
+
}
|
|
853
|
+
function resolveSameFileRefs(value, sameFile, path, suppressed, referencedSecrets, origins) {
|
|
762
854
|
if (typeof value === "string") {
|
|
763
855
|
const match = exactSameFileRef.exec(value.trim());
|
|
764
856
|
if (match) {
|
|
@@ -767,35 +859,67 @@ function resolveSameFileRefs(value, sameFile, path, suppressed, referencedSecret
|
|
|
767
859
|
return value;
|
|
768
860
|
if (typeof literal === "string")
|
|
769
861
|
noteResolvedLiteral(literal, path, sameFile, suppressed, referencedSecrets);
|
|
862
|
+
origins.set(path.join("."), `${match[1]}.${match[2]}`);
|
|
770
863
|
return literal;
|
|
771
864
|
}
|
|
772
865
|
const concatenated = resolveInterpolationString(value, sameFile);
|
|
773
866
|
if (concatenated !== undefined) {
|
|
774
|
-
noteResolvedLiteral(concatenated, path, sameFile, suppressed, referencedSecrets);
|
|
775
|
-
|
|
867
|
+
noteResolvedLiteral(concatenated.text, path, sameFile, suppressed, referencedSecrets);
|
|
868
|
+
recordResolvedSources(origins, path, concatenated.sources);
|
|
869
|
+
return concatenated.text;
|
|
776
870
|
}
|
|
777
871
|
const formatted = resolveFormatCall(value, sameFile);
|
|
778
872
|
if (formatted !== undefined) {
|
|
779
|
-
noteResolvedLiteral(formatted, path, sameFile, suppressed, referencedSecrets);
|
|
780
|
-
|
|
873
|
+
noteResolvedLiteral(formatted.text, path, sameFile, suppressed, referencedSecrets);
|
|
874
|
+
recordResolvedSources(origins, path, formatted.sources);
|
|
875
|
+
return formatted.text;
|
|
876
|
+
}
|
|
877
|
+
const joined = resolveJoinCall(value, sameFile);
|
|
878
|
+
if (joined !== undefined) {
|
|
879
|
+
noteResolvedLiteral(joined.text, path, sameFile, suppressed, referencedSecrets);
|
|
880
|
+
recordResolvedSources(origins, path, joined.sources);
|
|
881
|
+
return joined.text;
|
|
781
882
|
}
|
|
782
883
|
return value;
|
|
783
884
|
}
|
|
784
885
|
if (Array.isArray(value)) {
|
|
785
886
|
value.forEach((item, index) => {
|
|
786
|
-
value[index] = resolveSameFileRefs(item, sameFile, [...path, index], suppressed, referencedSecrets);
|
|
887
|
+
value[index] = resolveSameFileRefs(item, sameFile, [...path, index], suppressed, referencedSecrets, origins);
|
|
787
888
|
});
|
|
788
889
|
return value;
|
|
789
890
|
}
|
|
790
891
|
if (value && typeof value === "object") {
|
|
791
892
|
for (const [key, child] of Object.entries(value)) {
|
|
792
|
-
if (key === "__referencedSecrets")
|
|
893
|
+
if (key === "__referencedSecrets" || key === "__valueOrigins")
|
|
793
894
|
continue;
|
|
794
|
-
value[key] = resolveSameFileRefs(child, sameFile, [...path, key], suppressed, referencedSecrets);
|
|
895
|
+
value[key] = resolveSameFileRefs(child, sameFile, [...path, key], suppressed, referencedSecrets, origins);
|
|
795
896
|
}
|
|
796
897
|
}
|
|
797
898
|
return value;
|
|
798
899
|
}
|
|
900
|
+
function cloudFormationOrigins(resource) {
|
|
901
|
+
const stored = Object.getOwnPropertyDescriptor(resource.value, "__valueOrigins")?.value;
|
|
902
|
+
return stored instanceof Map ? stored : new Map();
|
|
903
|
+
}
|
|
904
|
+
function originSentence(origins, path) {
|
|
905
|
+
if (origins.size === 0)
|
|
906
|
+
return "";
|
|
907
|
+
const prefix = path.join(".");
|
|
908
|
+
const sources = new Set();
|
|
909
|
+
for (const [key, source] of origins) {
|
|
910
|
+
if (key === prefix || key.startsWith(`${prefix}.`))
|
|
911
|
+
sources.add(source);
|
|
912
|
+
}
|
|
913
|
+
if (sources.size !== 1)
|
|
914
|
+
return "";
|
|
915
|
+
return ` Value comes from ${[...sources][0]}.`;
|
|
916
|
+
}
|
|
917
|
+
function checkPackage(resource, check) {
|
|
918
|
+
if (resource.type === "package:script")
|
|
919
|
+
check("PKG001", resource.value.pipes === true, ["command"]);
|
|
920
|
+
else if (resource.type === "package:dependency")
|
|
921
|
+
check("PKG002", resource.value.range === "*" || resource.value.range === "latest", ["range"]);
|
|
922
|
+
}
|
|
799
923
|
function dedupeSecretFindings(findings) {
|
|
800
924
|
const seen = new Set();
|
|
801
925
|
return findings.filter((finding) => {
|
package/dist/iac/scan.d.ts
CHANGED
|
@@ -16,7 +16,7 @@ export declare function summarize(findings: Finding[]): {
|
|
|
16
16
|
};
|
|
17
17
|
export declare function filterFindings(findings: Finding[], options: ScanOptions): Finding[];
|
|
18
18
|
export declare function scanIac(content: string, options?: ScanOptions): {
|
|
19
|
-
status:
|
|
19
|
+
status: "completed";
|
|
20
20
|
engineVersion: string;
|
|
21
21
|
format: IacFormat;
|
|
22
22
|
focus: "security" | "reliability" | "cost" | "general";
|
package/dist/iac/scan.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { parseIac } from "./parse.js";
|
|
2
|
-
import { resourceFindings } from "./rules.js";
|
|
1
|
+
import { parseIac, readCloudFormationTables, ScanInputError } from "./parse.js";
|
|
2
|
+
import { resourceFindings, terraformLiterals } from "./rules.js";
|
|
3
3
|
import { VERSION } from "../version.js";
|
|
4
4
|
export const severityRank = {
|
|
5
5
|
info: 0,
|
|
@@ -8,6 +8,7 @@ export const severityRank = {
|
|
|
8
8
|
high: 3,
|
|
9
9
|
critical: 4,
|
|
10
10
|
};
|
|
11
|
+
const markerLine = /^----- skaleagents-file: (.+) -----$/;
|
|
11
12
|
export function summarize(findings) {
|
|
12
13
|
const bySeverity = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
|
|
13
14
|
for (const finding of findings)
|
|
@@ -24,29 +25,89 @@ export function filterFindings(findings, options) {
|
|
|
24
25
|
(a.location?.line ?? 0) - (b.location?.line ?? 0) ||
|
|
25
26
|
(a.ruleId ?? "").localeCompare(b.ruleId ?? ""));
|
|
26
27
|
}
|
|
27
|
-
|
|
28
|
-
const
|
|
29
|
-
const
|
|
30
|
-
|
|
28
|
+
function fileBasename(name) {
|
|
29
|
+
const trimmed = name.trim();
|
|
30
|
+
const base = trimmed.split(/[/\\]/).filter((part) => part.length > 0).pop() ?? "";
|
|
31
|
+
return base.trim();
|
|
32
|
+
}
|
|
33
|
+
function splitSubmittedFiles(content) {
|
|
34
|
+
const lines = content.split("\n");
|
|
35
|
+
const markers = [];
|
|
36
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
37
|
+
const line = lines[index].endsWith("\r") ? lines[index].slice(0, -1) : lines[index];
|
|
38
|
+
const match = markerLine.exec(line);
|
|
39
|
+
if (!match)
|
|
40
|
+
continue;
|
|
41
|
+
const name = fileBasename(match[1]);
|
|
42
|
+
if (!name || name.includes("----- skaleagents-file:"))
|
|
43
|
+
continue;
|
|
44
|
+
markers.push({ index, name });
|
|
45
|
+
}
|
|
46
|
+
if (markers.length === 0)
|
|
47
|
+
return undefined;
|
|
48
|
+
const parts = [];
|
|
49
|
+
const preamble = lines.slice(0, markers[0].index).join("\n");
|
|
50
|
+
if (preamble.trim())
|
|
51
|
+
parts.push({ name: "", text: preamble });
|
|
52
|
+
for (let index = 0; index < markers.length; index += 1) {
|
|
53
|
+
const start = markers[index].index + 1;
|
|
54
|
+
const end = index + 1 < markers.length ? markers[index + 1].index : lines.length;
|
|
55
|
+
parts.push({ name: markers[index].name, text: lines.slice(start, end).join("\n") });
|
|
56
|
+
}
|
|
57
|
+
return parts;
|
|
58
|
+
}
|
|
59
|
+
function labelResource(resource, fileName) {
|
|
60
|
+
if (!fileName)
|
|
61
|
+
return resource;
|
|
62
|
+
return {
|
|
63
|
+
id: `${fileName}:${resource.id}`,
|
|
64
|
+
type: resource.type,
|
|
65
|
+
value: resource.value,
|
|
66
|
+
locate: (path) => {
|
|
67
|
+
const location = resource.locate(path);
|
|
68
|
+
return { ...location, path: `${fileName}:${location.path}` };
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function mergeTables(parts) {
|
|
73
|
+
const defaults = new Map();
|
|
74
|
+
const secretNames = new Set();
|
|
75
|
+
const mappings = new Map();
|
|
76
|
+
for (const part of parts) {
|
|
77
|
+
for (const [name, value] of part.defaults)
|
|
78
|
+
if (!defaults.has(name))
|
|
79
|
+
defaults.set(name, value);
|
|
80
|
+
for (const name of part.secretNames)
|
|
81
|
+
secretNames.add(name);
|
|
82
|
+
for (const [name, value] of part.mappings)
|
|
83
|
+
if (!mappings.has(name))
|
|
84
|
+
mappings.set(name, value);
|
|
85
|
+
}
|
|
86
|
+
return { defaults, secretNames, mappings };
|
|
87
|
+
}
|
|
88
|
+
const sharedResolutionLimit = "Files in one audit share variable defaults, locals, and parameter defaults. join, Fn::Join, Fn::FindInMap, format, and a simple Fn::Sub resolve when every piece is a known literal. Other functions and module sources do not. A finding names var, local, or parameter when the value came from one.";
|
|
89
|
+
function presentScan(parsed, findings, checked, options) {
|
|
90
|
+
const visible = filterFindings(findings, options);
|
|
31
91
|
const limit = options.maxFindings ?? 100;
|
|
92
|
+
const sharesResolution = parsed.format === "terraform" || parsed.format === "cloudformation";
|
|
32
93
|
return {
|
|
33
94
|
status: "completed",
|
|
34
95
|
engineVersion: VERSION,
|
|
35
96
|
format: parsed.format,
|
|
36
97
|
focus: options.focus ?? "general",
|
|
37
|
-
summary: `Scanned ${parsed.resources.length} resources; ${
|
|
98
|
+
summary: `Scanned ${parsed.resources.length} resources; ${visible.length} findings match the selected filters.`,
|
|
38
99
|
parsedResourceCount: parsed.resources.length,
|
|
39
100
|
resources: parsed.resources.map((r) => ({
|
|
40
101
|
id: r.id,
|
|
41
102
|
type: r.type,
|
|
42
103
|
location: r.locate(),
|
|
43
104
|
})),
|
|
44
|
-
findings:
|
|
45
|
-
totals: summarize(
|
|
46
|
-
totalFindings:
|
|
47
|
-
truncated:
|
|
48
|
-
rulesEvaluated: [...
|
|
49
|
-
warnings: parsed.warnings,
|
|
105
|
+
findings: visible.slice(0, limit),
|
|
106
|
+
totals: summarize(visible),
|
|
107
|
+
totalFindings: visible.length,
|
|
108
|
+
truncated: visible.length > limit,
|
|
109
|
+
rulesEvaluated: [...checked].sort(),
|
|
110
|
+
warnings: [...new Set(parsed.warnings)],
|
|
50
111
|
limitations: [
|
|
51
112
|
"Static configuration review only. No cloud credentials, deployments, external modules, or runtime state are inspected.",
|
|
52
113
|
"Resource-specific checks cover Kubernetes workloads, RBAC and Secrets, plus AWS networking, IAM, S3, RDS, EC2 and EBS. Other resources receive literal credential and URL checks only.",
|
|
@@ -55,11 +116,61 @@ export function scanIac(content, options = {}) {
|
|
|
55
116
|
"HCL locations point to resource declarations; property paths identify the affected setting.",
|
|
56
117
|
]
|
|
57
118
|
: []),
|
|
58
|
-
...(
|
|
59
|
-
? [
|
|
60
|
-
"Same-file var, local, and CloudFormation parameter defaults are resolved. Functions, Fn::Sub, and module sources are not.",
|
|
61
|
-
]
|
|
62
|
-
: []),
|
|
119
|
+
...(sharesResolution ? [sharedResolutionLimit] : []),
|
|
63
120
|
],
|
|
64
121
|
};
|
|
65
122
|
}
|
|
123
|
+
function scanDocument(content, options) {
|
|
124
|
+
const parsed = parseIac(content, options.format);
|
|
125
|
+
const result = resourceFindings(parsed.resources, parsed.format);
|
|
126
|
+
return presentScan(parsed, result.findings, result.checked, options);
|
|
127
|
+
}
|
|
128
|
+
function scanMarked(parts, options) {
|
|
129
|
+
const tables = mergeTables(parts.map((part) => readCloudFormationTables(part.text)));
|
|
130
|
+
const parsedParts = [];
|
|
131
|
+
let lastError;
|
|
132
|
+
for (const part of parts) {
|
|
133
|
+
if (!part.text.trim())
|
|
134
|
+
continue;
|
|
135
|
+
try {
|
|
136
|
+
parsedParts.push({ name: part.name, parsed: parseIac(part.text, "auto", tables) });
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
lastError = error;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (parsedParts.length === 0) {
|
|
143
|
+
if (lastError instanceof Error)
|
|
144
|
+
throw lastError;
|
|
145
|
+
throw new ScanInputError("Content must not be empty or whitespace.");
|
|
146
|
+
}
|
|
147
|
+
const literals = terraformLiterals(parsedParts.flatMap((part) => part.parsed.resources));
|
|
148
|
+
const resources = [];
|
|
149
|
+
const findings = [];
|
|
150
|
+
const checked = new Set();
|
|
151
|
+
const warnings = [];
|
|
152
|
+
const formats = new Set();
|
|
153
|
+
for (const part of parsedParts) {
|
|
154
|
+
const labeled = part.parsed.resources.map((resource) => labelResource(resource, part.name));
|
|
155
|
+
const result = resourceFindings(labeled, part.parsed.format, literals);
|
|
156
|
+
resources.push(...labeled);
|
|
157
|
+
findings.push(...result.findings);
|
|
158
|
+
for (const id of result.checked)
|
|
159
|
+
checked.add(id);
|
|
160
|
+
warnings.push(...part.parsed.warnings);
|
|
161
|
+
formats.add(part.parsed.format);
|
|
162
|
+
}
|
|
163
|
+
const format = formats.size === 1 ? [...formats][0] : parsedParts[0].parsed.format;
|
|
164
|
+
const presented = presentScan({ format, resources, warnings }, findings, checked, options);
|
|
165
|
+
if (formats.has("terraform") || formats.has("cloudformation")) {
|
|
166
|
+
if (!presented.limitations.includes(sharedResolutionLimit))
|
|
167
|
+
presented.limitations.push(sharedResolutionLimit);
|
|
168
|
+
}
|
|
169
|
+
return presented;
|
|
170
|
+
}
|
|
171
|
+
export function scanIac(content, options = {}) {
|
|
172
|
+
const parts = splitSubmittedFiles(content);
|
|
173
|
+
if (!parts)
|
|
174
|
+
return scanDocument(content, options);
|
|
175
|
+
return scanMarked(parts, options);
|
|
176
|
+
}
|
package/dist/server.js
CHANGED
|
@@ -85,12 +85,13 @@ export function createServer(remote = false) {
|
|
|
85
85
|
"tfvars",
|
|
86
86
|
"serverless",
|
|
87
87
|
"iam",
|
|
88
|
+
"package",
|
|
88
89
|
"application",
|
|
89
90
|
"auto",
|
|
90
91
|
])
|
|
91
92
|
.optional()
|
|
92
93
|
.default("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."),
|
|
94
|
+
.describe("Content format: terraform, cloudformation, kubernetes, pulumi, compose, dockerfile, github, helm, ansible, bicep, arm, gitlab, azure-pipelines, cloudbuild, tfvars, serverless, iam, package, application, auto."),
|
|
94
95
|
},
|
|
95
96
|
}, async ({ content, focus, format, minSeverity, maxFindings }) => {
|
|
96
97
|
const botHints = fetchPublicBotHints();
|
|
@@ -138,7 +139,7 @@ export function createServer(remote = false) {
|
|
|
138
139
|
description: (name === "scan_iac_stub"
|
|
139
140
|
? "Compatibility alias for scan_iac; runs the full scanner. "
|
|
140
141
|
: "") +
|
|
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,
|
|
142
|
+
"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, a standalone IAM policy, and package.json. 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.",
|
|
142
143
|
annotations: {
|
|
143
144
|
readOnlyHint: true,
|
|
144
145
|
destructiveHint: false,
|
|
@@ -146,7 +147,7 @@ export function createServer(remote = false) {
|
|
|
146
147
|
},
|
|
147
148
|
_meta: { securitySchemes: [{ type: "oauth2", scopes: ["mcp"] }] },
|
|
148
149
|
inputSchema: {
|
|
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,
|
|
150
|
+
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, a standalone IAM policy, and package.json."),
|
|
150
151
|
format: z
|
|
151
152
|
.enum([
|
|
152
153
|
"terraform",
|
|
@@ -166,6 +167,7 @@ export function createServer(remote = false) {
|
|
|
166
167
|
"tfvars",
|
|
167
168
|
"serverless",
|
|
168
169
|
"iam",
|
|
170
|
+
"package",
|
|
169
171
|
"auto",
|
|
170
172
|
])
|
|
171
173
|
.default("auto"),
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.6.
|
|
1
|
+
export declare const VERSION = "0.6.16";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.6.
|
|
1
|
+
export const VERSION = "0.6.16";
|