@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 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 = "Terraform expressions are not evaluated. Findings use literal values and declared settings.";
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 (/(?:!(?:Ref|Sub|GetAtt|If)\b|"(?:Ref|Fn::\w+)"\s*:|\$\{)/.test(content))
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. Conditions are not evaluated.";
1447
- const iamPolicyWarning = "The IAM policy is not executed. Conditions are not evaluated.";
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
- if (!effectAllows(body) || !actionIsOnlyWildcard(body))
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: true },
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
- if (!effectAllows(body) || !actionIsOnlyWildcard(body))
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: true },
1609
- locate: () => lineLocation(line, `Statement.${index}.Action`),
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 actions = [...region.matchAll(/"Action"\s*:/g)];
1627
- const action = actions[index];
1628
- if (!action || action.index == null)
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 + action.index);
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), path);
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.13";
1
+ export declare const VERSION = "0.6.14";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const VERSION = "0.6.13";
1
+ export const VERSION = "0.6.14";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skaleagents/swarm",
3
- "version": "0.6.13",
3
+ "version": "0.6.14",
4
4
  "private": false,
5
5
  "publishConfig": {
6
6
  "access": "public"