@skaleagents/swarm 0.6.13 → 0.6.14
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.js +130 -17
- package/dist/iac/rules.js +115 -7
- package/dist/iac/scan.js +5 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
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 = "
|
|
142
|
+
const terraformExpressionWarning = "Same-file variable and local literals are resolved. Functions and other 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.");
|
|
@@ -412,11 +412,12 @@ export function parseIac(content, requested = "auto") {
|
|
|
412
412
|
Array.isArray(root.Resources) ||
|
|
413
413
|
typeof root.Resources !== "object")
|
|
414
414
|
throw new ScanInputError("CloudFormation requires a Resources mapping.");
|
|
415
|
+
const parameters = cloudFormationParameterLiterals(root);
|
|
415
416
|
for (const [name, raw] of Object.entries(object(root.Resources))) {
|
|
416
417
|
const value = object(raw);
|
|
417
418
|
if (typeof value.Type !== "string")
|
|
418
419
|
throw new ScanInputError("Each CloudFormation resource requires a Type.");
|
|
419
|
-
add(name, value.Type, value, ["Resources", name]);
|
|
420
|
+
add(name, value.Type, resolveCloudFormationRefs(value, parameters), ["Resources", name]);
|
|
420
421
|
}
|
|
421
422
|
}
|
|
422
423
|
else if (format === "terraform") {
|
|
@@ -467,7 +468,10 @@ export function parseIac(content, requested = "auto") {
|
|
|
467
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, or a standalone IAM policy.");
|
|
468
469
|
if (!resources.length)
|
|
469
470
|
warnings.push("No resources found in the submitted document.");
|
|
470
|
-
if (
|
|
471
|
+
if (format === "cloudformation" &&
|
|
472
|
+
/(?:!(?:Ref|Sub|GetAtt|If)\b|"(?:Ref|Fn::\w+)"\s*:|\bRef\s*:)/.test(content))
|
|
473
|
+
warnings.push("Same-file parameter defaults are resolved. Functions are not evaluated.");
|
|
474
|
+
else if (/(?:!(?:Ref|Sub|GetAtt|If)\b|"(?:Ref|Fn::\w+)"\s*:|\$\{)/.test(content))
|
|
471
475
|
warnings.push("Intrinsic functions and expressions are not evaluated. Only literal configuration is checked.");
|
|
472
476
|
return { format, resources, warnings };
|
|
473
477
|
}
|
|
@@ -1403,6 +1407,86 @@ function parseAzure(content, format) {
|
|
|
1403
1407
|
warnings: [azureExpressionWarning],
|
|
1404
1408
|
};
|
|
1405
1409
|
}
|
|
1410
|
+
function parameterNameIsSecret(name) {
|
|
1411
|
+
const normalized = name.replace(/-/g, "_");
|
|
1412
|
+
return (/(?:^|_)(?:password|passwd|secret|token)$/i.test(normalized) ||
|
|
1413
|
+
/(?:^|_)(?:api_?key|auth_token|access_key|private_key)$/i.test(normalized) ||
|
|
1414
|
+
/(?:password|passwd|secret|api[_-]?key|auth[_-]?token|access[_-]?key|private[_-]?key|token)$/i.test(name));
|
|
1415
|
+
}
|
|
1416
|
+
function cloudFormationParameterLiterals(root) {
|
|
1417
|
+
const defaults = new Map();
|
|
1418
|
+
const secretNames = new Set();
|
|
1419
|
+
for (const [name, raw] of Object.entries(object(root.Parameters))) {
|
|
1420
|
+
const body = object(raw);
|
|
1421
|
+
if (!("Default" in body))
|
|
1422
|
+
continue;
|
|
1423
|
+
const value = body.Default;
|
|
1424
|
+
if (typeof value !== "string" && typeof value !== "boolean" && typeof value !== "number")
|
|
1425
|
+
continue;
|
|
1426
|
+
defaults.set(name, value);
|
|
1427
|
+
if (typeof value === "string" && value.trim() !== "" && parameterNameIsSecret(name))
|
|
1428
|
+
secretNames.add(name);
|
|
1429
|
+
}
|
|
1430
|
+
return { defaults, secretNames };
|
|
1431
|
+
}
|
|
1432
|
+
function cloudFormationRefName(value) {
|
|
1433
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
1434
|
+
return undefined;
|
|
1435
|
+
const entries = Object.entries(value).filter(([key]) => key !== "__referencedSecrets");
|
|
1436
|
+
if (entries.length !== 1)
|
|
1437
|
+
return undefined;
|
|
1438
|
+
const [key, name] = entries[0];
|
|
1439
|
+
if (key !== "Ref" || typeof name !== "string" || name.trim() === "")
|
|
1440
|
+
return undefined;
|
|
1441
|
+
return name;
|
|
1442
|
+
}
|
|
1443
|
+
function cloudFormationIntrinsic(value) {
|
|
1444
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
1445
|
+
return false;
|
|
1446
|
+
return Object.keys(value).some((key) => key === "Fn::Sub" || key === "Fn::Join" || key === "Fn::FindInMap" || key.startsWith("Fn::"));
|
|
1447
|
+
}
|
|
1448
|
+
function resolveCloudFormationRefs(value, parameters) {
|
|
1449
|
+
const secretPaths = [];
|
|
1450
|
+
const resolved = resolveCloudFormationValue(value, parameters, [], secretPaths);
|
|
1451
|
+
if (secretPaths.length > 0 &&
|
|
1452
|
+
resolved &&
|
|
1453
|
+
typeof resolved === "object" &&
|
|
1454
|
+
!Array.isArray(resolved)) {
|
|
1455
|
+
Object.defineProperty(resolved, "__referencedSecrets", {
|
|
1456
|
+
enumerable: false,
|
|
1457
|
+
configurable: true,
|
|
1458
|
+
value: secretPaths,
|
|
1459
|
+
});
|
|
1460
|
+
}
|
|
1461
|
+
return object(resolved);
|
|
1462
|
+
}
|
|
1463
|
+
function resolveCloudFormationValue(value, parameters, path, secretPaths) {
|
|
1464
|
+
const ref = cloudFormationRefName(value);
|
|
1465
|
+
if (ref) {
|
|
1466
|
+
if (!parameters.defaults.has(ref))
|
|
1467
|
+
return value;
|
|
1468
|
+
const literal = parameters.defaults.get(ref);
|
|
1469
|
+
if (parameters.secretNames.has(ref) &&
|
|
1470
|
+
typeof literal === "string" &&
|
|
1471
|
+
literal.trim() !== "")
|
|
1472
|
+
secretPaths.push(path);
|
|
1473
|
+
return literal;
|
|
1474
|
+
}
|
|
1475
|
+
if (cloudFormationIntrinsic(value))
|
|
1476
|
+
return value;
|
|
1477
|
+
if (Array.isArray(value))
|
|
1478
|
+
return value.map((item, index) => resolveCloudFormationValue(item, parameters, [...path, index], secretPaths));
|
|
1479
|
+
if (value && typeof value === "object") {
|
|
1480
|
+
const out = {};
|
|
1481
|
+
for (const [key, child] of Object.entries(value)) {
|
|
1482
|
+
if (key === "__referencedSecrets")
|
|
1483
|
+
continue;
|
|
1484
|
+
out[key] = resolveCloudFormationValue(child, parameters, [...path, key], secretPaths);
|
|
1485
|
+
}
|
|
1486
|
+
return out;
|
|
1487
|
+
}
|
|
1488
|
+
return value;
|
|
1489
|
+
}
|
|
1406
1490
|
function terraformLocalBlocks(data) {
|
|
1407
1491
|
const raw = object(data).locals;
|
|
1408
1492
|
if (Array.isArray(raw))
|
|
@@ -1443,8 +1527,8 @@ export function looksLikeIamPolicy(content) {
|
|
|
1443
1527
|
const statement = root.Statement ?? root.statement;
|
|
1444
1528
|
return Array.isArray(statement);
|
|
1445
1529
|
}
|
|
1446
|
-
const serverlessWarning = "The Serverless Framework config is not executed.
|
|
1447
|
-
const iamPolicyWarning = "The IAM policy is not executed.
|
|
1530
|
+
const serverlessWarning = "The Serverless Framework config is not executed. Other condition operators are not evaluated.";
|
|
1531
|
+
const iamPolicyWarning = "The IAM policy is not executed. Other condition operators are not evaluated.";
|
|
1448
1532
|
function serverlessShapeError() {
|
|
1449
1533
|
return "Serverless Framework config requires top-level service and provider, plus functions or plugins.";
|
|
1450
1534
|
}
|
|
@@ -1457,6 +1541,29 @@ function effectAllows(body) {
|
|
|
1457
1541
|
return true;
|
|
1458
1542
|
return String(effect).trim().toLowerCase() === "allow";
|
|
1459
1543
|
}
|
|
1544
|
+
const openSourceAddresses = new Set(["0.0.0.0/0", "::/0", "*"]);
|
|
1545
|
+
function conditionValueIsOpen(value) {
|
|
1546
|
+
if (typeof value === "string")
|
|
1547
|
+
return openSourceAddresses.has(value.trim());
|
|
1548
|
+
if (Array.isArray(value))
|
|
1549
|
+
return value.some((item) => conditionValueIsOpen(item));
|
|
1550
|
+
return false;
|
|
1551
|
+
}
|
|
1552
|
+
function sourceAddressAllowsEveryone(body) {
|
|
1553
|
+
if (!effectAllows(body))
|
|
1554
|
+
return false;
|
|
1555
|
+
const condition = object(body.Condition ?? body.condition);
|
|
1556
|
+
let ipAddress;
|
|
1557
|
+
for (const [key, value] of Object.entries(condition)) {
|
|
1558
|
+
if (key.toLowerCase() === "ipaddress") {
|
|
1559
|
+
ipAddress = value;
|
|
1560
|
+
break;
|
|
1561
|
+
}
|
|
1562
|
+
}
|
|
1563
|
+
if (!ipAddress || typeof ipAddress !== "object" || Array.isArray(ipAddress))
|
|
1564
|
+
return false;
|
|
1565
|
+
return Object.values(object(ipAddress)).some((value) => conditionValueIsOpen(value));
|
|
1566
|
+
}
|
|
1460
1567
|
function actionIsOnlyWildcard(body) {
|
|
1461
1568
|
const action = body.Action ?? body.action ?? body.actions ?? body.Actions;
|
|
1462
1569
|
if (typeof action === "string")
|
|
@@ -1531,14 +1638,16 @@ function parseServerless(content) {
|
|
|
1531
1638
|
for (const group of statementGroups(root)) {
|
|
1532
1639
|
group.statements.forEach((statement, index) => {
|
|
1533
1640
|
const body = object(statement);
|
|
1534
|
-
|
|
1641
|
+
const wildcard = effectAllows(body) && actionIsOnlyWildcard(body);
|
|
1642
|
+
const openSourceIp = sourceAddressAllowsEveryone(body);
|
|
1643
|
+
if (!wildcard && !openSourceIp)
|
|
1535
1644
|
return;
|
|
1536
1645
|
iamCount += 1;
|
|
1537
|
-
const path = [...group.path, index, "Action"];
|
|
1646
|
+
const path = [...group.path, index, openSourceIp && !wildcard ? "Condition" : "Action"];
|
|
1538
1647
|
resources.push({
|
|
1539
1648
|
id: `serverless.iam.${iamCount}`,
|
|
1540
1649
|
type: "serverless:iam",
|
|
1541
|
-
value: { wildcard
|
|
1650
|
+
value: { wildcard, openSourceIp },
|
|
1542
1651
|
locate: () => yamlLocation(doc, lines, path),
|
|
1543
1652
|
});
|
|
1544
1653
|
});
|
|
@@ -1599,14 +1708,18 @@ function parseIamPolicy(content) {
|
|
|
1599
1708
|
const resources = [];
|
|
1600
1709
|
statements.forEach((statement, index) => {
|
|
1601
1710
|
const body = object(statement);
|
|
1602
|
-
|
|
1711
|
+
const wildcard = effectAllows(body) && actionIsOnlyWildcard(body);
|
|
1712
|
+
const openSourceIp = sourceAddressAllowsEveryone(body);
|
|
1713
|
+
if (!wildcard && !openSourceIp)
|
|
1603
1714
|
return;
|
|
1604
|
-
const line = iamStatementLine(content, index);
|
|
1715
|
+
const line = iamStatementLine(content, index, openSourceIp && !wildcard ? "Condition" : "Action");
|
|
1605
1716
|
resources.push({
|
|
1606
1717
|
id: `iam.statement.${index + 1}`,
|
|
1607
1718
|
type: "iam:statement",
|
|
1608
|
-
value: { wildcard
|
|
1609
|
-
locate: () => lineLocation(line,
|
|
1719
|
+
value: { wildcard, openSourceIp },
|
|
1720
|
+
locate: () => lineLocation(line, openSourceIp && !wildcard
|
|
1721
|
+
? `Statement.${index}.Condition`
|
|
1722
|
+
: `Statement.${index}.Action`),
|
|
1610
1723
|
});
|
|
1611
1724
|
});
|
|
1612
1725
|
if (!resources.length) {
|
|
@@ -1619,13 +1732,13 @@ function parseIamPolicy(content) {
|
|
|
1619
1732
|
}
|
|
1620
1733
|
return { format: "iam", resources, warnings: [iamPolicyWarning] };
|
|
1621
1734
|
}
|
|
1622
|
-
function iamStatementLine(content, index) {
|
|
1735
|
+
function iamStatementLine(content, index, key = "Action") {
|
|
1623
1736
|
const matches = [...content.matchAll(/"Statement"\s*:/g)];
|
|
1624
1737
|
const start = matches[0]?.index ?? 0;
|
|
1625
1738
|
const region = content.slice(start);
|
|
1626
|
-
const
|
|
1627
|
-
const
|
|
1628
|
-
if (!
|
|
1739
|
+
const keys = [...region.matchAll(new RegExp(`"${key}"\\s*:`, "g"))];
|
|
1740
|
+
const hit = key === "Action" ? keys[index] : keys.find((item) => item.index != null);
|
|
1741
|
+
if (!hit || hit.index == null)
|
|
1629
1742
|
return lineAt(content, start);
|
|
1630
|
-
return lineAt(content, start +
|
|
1743
|
+
return lineAt(content, start + hit.index);
|
|
1631
1744
|
}
|
package/dist/iac/rules.js
CHANGED
|
@@ -369,6 +369,22 @@ export const rules = {
|
|
|
369
369
|
export function resourceFindings(resources, format) {
|
|
370
370
|
const findings = [];
|
|
371
371
|
const checked = new Set();
|
|
372
|
+
const sameFile = collectSameFileLiterals(resources);
|
|
373
|
+
const suppressedSecrets = new WeakMap();
|
|
374
|
+
const moduleSecrets = new WeakMap();
|
|
375
|
+
if (format === "terraform") {
|
|
376
|
+
for (const resource of resources) {
|
|
377
|
+
if (resource.type === "terraform:variable" ||
|
|
378
|
+
resource.type === "terraform:local" ||
|
|
379
|
+
resource.type === "tfvars:assignment")
|
|
380
|
+
continue;
|
|
381
|
+
const suppressed = new Set();
|
|
382
|
+
const referenced = new Set();
|
|
383
|
+
resolveSameFileRefs(resource.value, sameFile, [], suppressed, referenced);
|
|
384
|
+
suppressedSecrets.set(resource, suppressed);
|
|
385
|
+
moduleSecrets.set(resource, referenced);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
372
388
|
for (const resource of resources) {
|
|
373
389
|
const check = (id, condition, path = []) => {
|
|
374
390
|
checked.add(id);
|
|
@@ -412,6 +428,7 @@ export function resourceFindings(resources, format) {
|
|
|
412
428
|
}
|
|
413
429
|
if (format === "iam") {
|
|
414
430
|
check("IAM001", resource.value.wildcard === true, ["Action"]);
|
|
431
|
+
check("NET001", resource.value.openSourceIp === true, ["Condition"]);
|
|
415
432
|
continue;
|
|
416
433
|
}
|
|
417
434
|
if (resource.type === "terraform:local") {
|
|
@@ -419,7 +436,7 @@ export function resourceFindings(resources, format) {
|
|
|
419
436
|
continue;
|
|
420
437
|
}
|
|
421
438
|
if (resource.type === "terraform:module") {
|
|
422
|
-
checkTerraformLiterals(resource.value, "module", check, []);
|
|
439
|
+
checkTerraformLiterals(resource.value, "module", check, [], "", moduleSecrets.get(resource));
|
|
423
440
|
continue;
|
|
424
441
|
}
|
|
425
442
|
if (resource.type === "terraform:variable") {
|
|
@@ -436,7 +453,8 @@ export function resourceFindings(resources, format) {
|
|
|
436
453
|
const credentialName = /(?:password|passwd|secret|api[_-]?key|auth[_-]?token)$/i;
|
|
437
454
|
check("SEC001", (credentialName.test(key) ||
|
|
438
455
|
(key === "value" && credentialName.test(String(parent.name)))) &&
|
|
439
|
-
literalCredential(value)
|
|
456
|
+
literalCredential(value) &&
|
|
457
|
+
!suppressedSecrets.get(resource)?.has(path.join(".")), path);
|
|
440
458
|
check("SEC002", /\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/.test(value), path);
|
|
441
459
|
check("SEC003", /-----BEGIN (?:RSA |EC |OPENSSH |DSA |)PRIVATE KEY-----/.test(value), path);
|
|
442
460
|
check("NET002", [...value.matchAll(/\bhttp:\/\/[^\s"'<>]+/gi)].some((m) => externalHttp(m[0])), path);
|
|
@@ -447,8 +465,15 @@ export function resourceFindings(resources, format) {
|
|
|
447
465
|
checkCompose(resource, check);
|
|
448
466
|
else
|
|
449
467
|
checkCloud(resource, resources, format, check);
|
|
468
|
+
const referencedSecrets = Object.getOwnPropertyDescriptor(resource.value, "__referencedSecrets")?.value;
|
|
469
|
+
if (Array.isArray(referencedSecrets)) {
|
|
470
|
+
for (const secretPath of referencedSecrets) {
|
|
471
|
+
if (Array.isArray(secretPath))
|
|
472
|
+
check("SEC001", true, secretPath);
|
|
473
|
+
}
|
|
474
|
+
}
|
|
450
475
|
}
|
|
451
|
-
return { findings, checked };
|
|
476
|
+
return { findings: dedupeSecretFindings(findings), checked };
|
|
452
477
|
}
|
|
453
478
|
function checkDockerfile(resource, check) {
|
|
454
479
|
if (resource.type === "dockerfile:user")
|
|
@@ -500,8 +525,10 @@ function checkPipeline(resource, check) {
|
|
|
500
525
|
check("PIPE003", resource.value.present === true, ["privileged"]);
|
|
501
526
|
}
|
|
502
527
|
function checkServerless(resource, check) {
|
|
503
|
-
if (resource.type === "serverless:iam")
|
|
528
|
+
if (resource.type === "serverless:iam") {
|
|
504
529
|
check("IAM001", resource.value.wildcard === true, ["Action"]);
|
|
530
|
+
check("NET001", resource.value.openSourceIp === true, ["Condition"]);
|
|
531
|
+
}
|
|
505
532
|
else if (resource.type === "serverless:env")
|
|
506
533
|
check("SLS001", resource.value.literal === true, ["environment"]);
|
|
507
534
|
}
|
|
@@ -520,7 +547,7 @@ function moduleCidrExempt(name) {
|
|
|
520
547
|
normalized === "destination" ||
|
|
521
548
|
normalized === "ipv6_egress");
|
|
522
549
|
}
|
|
523
|
-
function checkTerraformLiterals(value, mode, check, path, name = "") {
|
|
550
|
+
function checkTerraformLiterals(value, mode, check, path, name = "", referencedSecrets) {
|
|
524
551
|
if (typeof value === "string" || typeof value === "boolean") {
|
|
525
552
|
const open = value === "0.0.0.0/0" || value === "::/0";
|
|
526
553
|
const exempt = mode === "assignment" ? name.toLowerCase() === "egress" : moduleCidrExempt(name);
|
|
@@ -532,16 +559,97 @@ function checkTerraformLiterals(value, mode, check, path, name = "") {
|
|
|
532
559
|
check("TF001", value.trim() !== "" && !terraformReference(value), path);
|
|
533
560
|
if (mode === "local" && typeof value === "string" && secretAssignmentName(name))
|
|
534
561
|
check("TF002", value.trim() !== "" && !terraformReference(value), path);
|
|
562
|
+
if (mode === "module" && referencedSecrets?.has(path.join(".")))
|
|
563
|
+
check("SEC001", true, path);
|
|
535
564
|
return;
|
|
536
565
|
}
|
|
537
566
|
if (Array.isArray(value)) {
|
|
538
|
-
value.forEach((item, index) => checkTerraformLiterals(item, mode, check, [...path, index], name));
|
|
567
|
+
value.forEach((item, index) => checkTerraformLiterals(item, mode, check, [...path, index], name, referencedSecrets));
|
|
539
568
|
return;
|
|
540
569
|
}
|
|
541
570
|
if (value && typeof value === "object") {
|
|
542
571
|
for (const [key, child] of Object.entries(value))
|
|
543
|
-
checkTerraformLiterals(child, mode, check, [...path, key], key);
|
|
572
|
+
checkTerraformLiterals(child, mode, check, [...path, key], key, referencedSecrets);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
const exactSameFileRef = /^\$\{(var|local)\.([A-Za-z_][A-Za-z0-9_]*)\}$/;
|
|
576
|
+
function sameFileLiteral(value) {
|
|
577
|
+
if (typeof value === "boolean" || typeof value === "number")
|
|
578
|
+
return value;
|
|
579
|
+
if (typeof value === "string" && !value.includes("${"))
|
|
580
|
+
return value;
|
|
581
|
+
return undefined;
|
|
582
|
+
}
|
|
583
|
+
function collectSameFileLiterals(resources) {
|
|
584
|
+
const vars = new Map();
|
|
585
|
+
const locals = new Map();
|
|
586
|
+
const declaredSecrets = new Set();
|
|
587
|
+
for (const resource of resources) {
|
|
588
|
+
if (resource.type === "terraform:variable") {
|
|
589
|
+
const name = String(resource.value.name ?? "");
|
|
590
|
+
const literal = sameFileLiteral(resource.value.default);
|
|
591
|
+
if (!name || literal === undefined)
|
|
592
|
+
continue;
|
|
593
|
+
vars.set(name, literal);
|
|
594
|
+
if (typeof literal === "string" && literal.trim() !== "" && secretAssignmentName(name))
|
|
595
|
+
declaredSecrets.add(literal);
|
|
596
|
+
}
|
|
597
|
+
if (resource.type === "terraform:local") {
|
|
598
|
+
for (const [name, raw] of Object.entries(resource.value)) {
|
|
599
|
+
const literal = sameFileLiteral(raw);
|
|
600
|
+
if (literal === undefined)
|
|
601
|
+
continue;
|
|
602
|
+
locals.set(name, literal);
|
|
603
|
+
if (typeof literal === "string" && literal.trim() !== "" && secretAssignmentName(name))
|
|
604
|
+
declaredSecrets.add(literal);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
544
607
|
}
|
|
608
|
+
return { vars, locals, declaredSecrets };
|
|
609
|
+
}
|
|
610
|
+
function resolveSameFileRefs(value, sameFile, path, suppressed, referencedSecrets) {
|
|
611
|
+
if (typeof value === "string") {
|
|
612
|
+
const match = exactSameFileRef.exec(value.trim());
|
|
613
|
+
if (!match)
|
|
614
|
+
return value;
|
|
615
|
+
const literal = (match[1] === "var" ? sameFile.vars : sameFile.locals).get(match[2]);
|
|
616
|
+
if (literal === undefined)
|
|
617
|
+
return value;
|
|
618
|
+
if (typeof literal === "string" && literal.trim() !== "") {
|
|
619
|
+
const key = [...path].reverse().find((part) => typeof part === "string");
|
|
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("."));
|
|
624
|
+
}
|
|
625
|
+
return literal;
|
|
626
|
+
}
|
|
627
|
+
if (Array.isArray(value)) {
|
|
628
|
+
value.forEach((item, index) => {
|
|
629
|
+
value[index] = resolveSameFileRefs(item, sameFile, [...path, index], suppressed, referencedSecrets);
|
|
630
|
+
});
|
|
631
|
+
return value;
|
|
632
|
+
}
|
|
633
|
+
if (value && typeof value === "object") {
|
|
634
|
+
for (const [key, child] of Object.entries(value)) {
|
|
635
|
+
if (key === "__referencedSecrets")
|
|
636
|
+
continue;
|
|
637
|
+
value[key] = resolveSameFileRefs(child, sameFile, [...path, key], suppressed, referencedSecrets);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return value;
|
|
641
|
+
}
|
|
642
|
+
function dedupeSecretFindings(findings) {
|
|
643
|
+
const seen = new Set();
|
|
644
|
+
return findings.filter((finding) => {
|
|
645
|
+
if (finding.ruleId !== "SEC001")
|
|
646
|
+
return true;
|
|
647
|
+
const key = `${finding.ruleId}|${finding.resource}|${finding.location?.path ?? ""}`;
|
|
648
|
+
if (seen.has(key))
|
|
649
|
+
return false;
|
|
650
|
+
seen.add(key);
|
|
651
|
+
return true;
|
|
652
|
+
});
|
|
545
653
|
}
|
|
546
654
|
function checkAzure(resource, check) {
|
|
547
655
|
if (resource.type === "azure:exposure")
|
package/dist/iac/scan.js
CHANGED
|
@@ -55,6 +55,11 @@ export function scanIac(content, options = {}) {
|
|
|
55
55
|
"HCL locations point to resource declarations; property paths identify the affected setting.",
|
|
56
56
|
]
|
|
57
57
|
: []),
|
|
58
|
+
...(parsed.format === "terraform" || parsed.format === "cloudformation"
|
|
59
|
+
? [
|
|
60
|
+
"Same-file var, local, and CloudFormation parameter defaults are resolved. Functions, Fn::Sub, and module sources are not.",
|
|
61
|
+
]
|
|
62
|
+
: []),
|
|
58
63
|
],
|
|
59
64
|
};
|
|
60
65
|
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.6.
|
|
1
|
+
export declare const VERSION = "0.6.14";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = "0.6.
|
|
1
|
+
export const VERSION = "0.6.14";
|