@intentius/chant-lexicon-terraform 0.124.31 → 0.124.32

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.
Files changed (141) hide show
  1. package/dist/__fixtures__/terragrunt/affected-cases.js +111 -0
  2. package/dist/__fixtures__/terragrunt/record.js +57 -0
  3. package/dist/behaviour/index.js +6 -0
  4. package/dist/behaviour/kinds.js +194 -0
  5. package/dist/behaviour/predict.js +297 -0
  6. package/dist/behaviour/reference-catalog.js +178 -0
  7. package/dist/behaviour/request.js +482 -0
  8. package/dist/change-set.js +234 -0
  9. package/dist/changed-roots.js +97 -0
  10. package/dist/codegen/docs-cli.js +3 -0
  11. package/dist/codegen/docs.js +44 -0
  12. package/dist/codegen/generate-cli.js +9 -0
  13. package/dist/codegen/generate.js +59 -0
  14. package/dist/codegen/package.js +43 -0
  15. package/dist/commands.js +106 -0
  16. package/dist/components/capability-plugin.d.ts +38 -2
  17. package/dist/components/capability-plugin.d.ts.map +1 -1
  18. package/dist/components/capability-plugin.js +260 -0
  19. package/dist/composites/terraform-adopt-op.js +152 -0
  20. package/dist/composites/terraform-apply-op.js +183 -0
  21. package/dist/composites/terraform-pin-rollout-op.js +45 -0
  22. package/dist/composites/terraform-watch-op.js +219 -0
  23. package/dist/config.js +118 -0
  24. package/dist/describe-resources.js +984 -0
  25. package/dist/generated/index.js +1 -0
  26. package/dist/hcl/descend.js +208 -0
  27. package/dist/hcl/edges.js +287 -0
  28. package/dist/hcl/jsonencode.js +442 -0
  29. package/dist/hcl/marker.js +212 -0
  30. package/dist/hcl/parse.js +372 -0
  31. package/dist/hcl/references.js +156 -0
  32. package/dist/hcl/roots.js +98 -0
  33. package/dist/hcl/suppressions.js +189 -0
  34. package/dist/hcl/value.js +72 -0
  35. package/dist/index.d.ts +1 -1
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +46 -0
  38. package/dist/integrity.json +2 -2
  39. package/dist/lint/audit-catalog.js +501 -0
  40. package/dist/lint/audit-lineage.js +442 -0
  41. package/dist/lint/post-synth/blocks.js +87 -0
  42. package/dist/lint/post-synth/fixtures/load.js +94 -0
  43. package/dist/lint/post-synth/flat-attributes.js +135 -0
  44. package/dist/lint/post-synth/index.js +90 -0
  45. package/dist/lint/post-synth/module-source.js +153 -0
  46. package/dist/lint/post-synth/scope.js +104 -0
  47. package/dist/lint/post-synth/terragrunt.js +63 -0
  48. package/dist/lint/post-synth/tf001.js +138 -0
  49. package/dist/lint/post-synth/tf002.js +143 -0
  50. package/dist/lint/post-synth/tf003.js +60 -0
  51. package/dist/lint/post-synth/tf004.js +54 -0
  52. package/dist/lint/post-synth/tf005.js +68 -0
  53. package/dist/lint/post-synth/tf006.js +43 -0
  54. package/dist/lint/post-synth/tf007.js +66 -0
  55. package/dist/lint/post-synth/tf008.js +64 -0
  56. package/dist/lint/post-synth/tf009.js +49 -0
  57. package/dist/lint/post-synth/tf010.js +40 -0
  58. package/dist/lint/post-synth/tf011.js +38 -0
  59. package/dist/lint/post-synth/tf012.js +36 -0
  60. package/dist/lint/post-synth/tf013.js +53 -0
  61. package/dist/lint/post-synth/tf014.js +61 -0
  62. package/dist/lint/post-synth/tf015.js +62 -0
  63. package/dist/lint/post-synth/tf016.js +66 -0
  64. package/dist/lint/post-synth/tf017.js +39 -0
  65. package/dist/lint/post-synth/tf018.js +63 -0
  66. package/dist/lint/post-synth/tf019.js +60 -0
  67. package/dist/lint/post-synth/tf020.js +103 -0
  68. package/dist/lint/post-synth/tf021.js +117 -0
  69. package/dist/lint/post-synth/tf022.js +51 -0
  70. package/dist/lint/post-synth/tf024.js +62 -0
  71. package/dist/lint/post-synth/tf025.js +83 -0
  72. package/dist/lint/post-synth/tf026.js +74 -0
  73. package/dist/lint/post-synth/tf027.js +66 -0
  74. package/dist/lint/post-synth/tf028.js +83 -0
  75. package/dist/lint/post-synth/tf029.js +73 -0
  76. package/dist/lint/post-synth/tf030.js +253 -0
  77. package/dist/lint/post-synth/tf031.js +332 -0
  78. package/dist/lint/post-synth/tf032.js +201 -0
  79. package/dist/lint/post-synth/tf033.js +126 -0
  80. package/dist/lint/post-synth/tf034.js +45 -0
  81. package/dist/lint/post-synth/tf035.js +63 -0
  82. package/dist/lint/post-synth/tf036.js +97 -0
  83. package/dist/lint/post-synth/tf037.js +80 -0
  84. package/dist/lint/post-synth/tf038.js +63 -0
  85. package/dist/lint/post-synth/tf039.js +55 -0
  86. package/dist/lint/post-synth/tf040.js +67 -0
  87. package/dist/lint/post-synth/tf041.js +53 -0
  88. package/dist/lint/post-synth/tf042.js +38 -0
  89. package/dist/lint/post-synth/tf043.js +45 -0
  90. package/dist/lint/post-synth/tf044.js +45 -0
  91. package/dist/lint/post-synth/tf045.js +44 -0
  92. package/dist/lint/rules/fixtures/load.js +50 -0
  93. package/dist/lint/rules/index.js +4 -0
  94. package/dist/lint/rules/plan-before-apply.js +136 -0
  95. package/dist/lint/secret-shape.js +149 -0
  96. package/dist/live-ownership.js +57 -0
  97. package/dist/lsp/completions.js +95 -0
  98. package/dist/lsp/context.js +358 -0
  99. package/dist/lsp/hover.js +45 -0
  100. package/dist/lsp/option-keys.js +93 -0
  101. package/dist/manifest.json +1 -1
  102. package/dist/op/activities/index.js +30 -0
  103. package/dist/op/activities/live-detect.js +143 -0
  104. package/dist/op/activities/pin-rollout.js +45 -0
  105. package/dist/op/activities/terraform.js +1270 -0
  106. package/dist/op/activity-contracts.js +162 -0
  107. package/dist/op/adoption.js +272 -0
  108. package/dist/op/builders.js +125 -0
  109. package/dist/op/resolve-root-mode.js +77 -0
  110. package/dist/package-cli.js +16 -0
  111. package/dist/pin/edit-ts.js +76 -0
  112. package/dist/pin/edit.js +118 -0
  113. package/dist/pin/forge.js +65 -0
  114. package/dist/pin/index.js +16 -0
  115. package/dist/pin/rollout.d.ts.map +1 -1
  116. package/dist/pin/rollout.js +382 -0
  117. package/dist/pin/scan.js +287 -0
  118. package/dist/pin/source.js +171 -0
  119. package/dist/pin/waves.js +109 -0
  120. package/dist/plan-digest.js +67 -0
  121. package/dist/plugin.js +323 -0
  122. package/dist/serializer.js +23 -0
  123. package/dist/state-ownership.js +29 -0
  124. package/dist/terragrunt/affected.d.ts.map +1 -1
  125. package/dist/terragrunt/affected.js +290 -0
  126. package/dist/terragrunt/index.js +10 -0
  127. package/dist/terragrunt/mocks.js +270 -0
  128. package/dist/terragrunt/run.js +264 -0
  129. package/dist/terragrunt/units.js +195 -0
  130. package/dist/terragrunt/wave.js +125 -0
  131. package/dist/validate-cli.js +4 -0
  132. package/dist/validate.js +31 -0
  133. package/package.json +10 -10
  134. package/src/codegen/package.ts +2 -2
  135. package/src/components/capability-plugin.test.ts +97 -2
  136. package/src/components/capability-plugin.ts +117 -12
  137. package/src/components/pr-loop.tofu.test.ts +51 -11
  138. package/src/index.ts +1 -0
  139. package/src/pin/rollout.ts +2 -1
  140. package/src/terragrunt/affected.test.ts +11 -0
  141. package/src/terragrunt/affected.ts +2 -1
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Shared reading helpers for TF033-TF037, the five flat encryption and
3
+ * immutability rules (chant #2288, epic #2284).
4
+ *
5
+ * Each of those rules reads one or two scalar attributes of one resource type
6
+ * and answers a three-valued question: the literal proves the resource
7
+ * secure, the literal proves it insecure, or the parse cannot tell. This
8
+ * module is the reading half; each rule file holds its own answer for
9
+ * absence, because that answer differs per resource (see each rule's doc
10
+ * comment and its section of `docs/pages/lint-rules.mdx`). It exports no
11
+ * `PostSynthCheck`, so the generated barrel skips it.
12
+ */
13
+ import { RESOURCE_TYPE } from "../../hcl/parse.js";
14
+ import { attr } from "../../hcl/value.js";
15
+ import { blocksOfType, nestedBodies } from "./blocks.js";
16
+ /** Every `resource` block of one provider type (`aws_sqs_queue`), in entity order. */
17
+ export function resourcesOfType(ctx, type) {
18
+ return blocksOfType(ctx, RESOURCE_TYPE).filter((b) => b.address.startsWith(`${type}.`));
19
+ }
20
+ /** One expression, for a message: `var.encrypted`, or the template text. */
21
+ function describe(name, raw, refs, kind) {
22
+ return kind === "reference"
23
+ ? `\`${name}\` is an expression (${refs?.[0] ?? String(raw)})`
24
+ : `\`${name}\` is a template string ("${String(raw)}")`;
25
+ }
26
+ /**
27
+ * A bool attribute. Terraform converts the strings `"true"` and `"false"` to
28
+ * bool, so those read as the bool they convert to. Any other literal (a
29
+ * number, a list) is a plan-time type error, not this rule's concern, and
30
+ * reads as unknown.
31
+ */
32
+ export function readBool(body, name) {
33
+ const a = attr(body, name);
34
+ if (a.kind === "absent")
35
+ return { kind: "absent" };
36
+ if (a.kind === "reference" || a.kind === "template") {
37
+ return { kind: "unknown", reason: describe(name, a.raw, a.refs, a.kind) };
38
+ }
39
+ if (typeof a.value === "boolean")
40
+ return { kind: "known", value: a.value };
41
+ if (a.value === "true" || a.value === "false")
42
+ return { kind: "known", value: a.value === "true" };
43
+ return { kind: "unknown", reason: `\`${name}\` is not a bool (${JSON.stringify(a.value)})` };
44
+ }
45
+ /** A string attribute, literal or not. */
46
+ export function readString(body, name) {
47
+ const a = attr(body, name);
48
+ if (a.kind === "absent")
49
+ return { kind: "absent" };
50
+ if (a.kind === "reference" || a.kind === "template") {
51
+ return { kind: "unknown", reason: describe(name, a.raw, a.refs, a.kind) };
52
+ }
53
+ if (typeof a.value === "string")
54
+ return { kind: "known", value: a.value };
55
+ return { kind: "unknown", reason: `\`${name}\` is not a string (${JSON.stringify(a.value)})` };
56
+ }
57
+ /** Expression roots that name an input or a computed value, not a managed object. */
58
+ const VALUE_ROOTS = new Set(["var", "local", "module", "each", "count", "path", "terraform", "self"]);
59
+ /** `aws_kms_key.sns.arn`, `data.aws_kms_alias.sqs.target_key_arn`, with an optional index or splat. */
60
+ const OBJECT_ATTRIBUTE = /^(?:data\.)?([a-z][a-z0-9_]*)\.[A-Za-z_][\w-]*(?:\[[^\]]*\])?(?:\.[A-Za-z_][\w-]*|\[[^\]]*\])+$/;
61
+ /** Every `${...}` span in a template, as `attr()` finds them. */
62
+ const SPANS = /\$\{(?:[^{}]|\{[^{}]*\})*\}/g;
63
+ /**
64
+ * Whether a key-like argument (`kms_master_key_id`) is set to something.
65
+ *
66
+ * - absent, or `null`: `absent`.
67
+ * - a literal: `known`, with the string (`""` is a value, and a rule decides
68
+ * what an empty key means).
69
+ * - an attribute of a managed resource or data source (`aws_kms_key.x.arn`):
70
+ * `known` and non-empty. The provider computes a key's id or ARN; it is
71
+ * never the empty string, so the key is set whatever its value turns out to
72
+ * be.
73
+ * - a template with literal text (`"alias/${var.env}-sns"`): `known` and
74
+ * non-empty, for the same reason.
75
+ * - anything else (`var.kms_key`, `local.key`, a conditional, a function
76
+ * call): `unknown`. A variable can be `null` or `""`.
77
+ */
78
+ export function readKeySet(body, name) {
79
+ const a = attr(body, name);
80
+ if (a.kind === "absent")
81
+ return { kind: "absent" };
82
+ if (a.kind === "literal") {
83
+ return typeof a.value === "string"
84
+ ? { kind: "known", value: a.value }
85
+ : { kind: "unknown", reason: `\`${name}\` is not a string (${JSON.stringify(a.value)})` };
86
+ }
87
+ const raw = String(a.raw);
88
+ if (a.kind === "template") {
89
+ if (raw.replace(SPANS, "").length > 0)
90
+ return { kind: "known", value: raw };
91
+ return { kind: "unknown", reason: describe(name, a.raw, a.refs, "template") };
92
+ }
93
+ const expr = (a.refs?.[0] ?? "").trim();
94
+ const m = OBJECT_ATTRIBUTE.exec(expr);
95
+ if (m && !VALUE_ROOTS.has(m[1]))
96
+ return { kind: "known", value: raw };
97
+ return { kind: "unknown", reason: describe(name, a.raw, a.refs, "reference") };
98
+ }
99
+ /**
100
+ * Whether an argument that names a source (`snapshot_identifier`,
101
+ * `replicate_source_db`) or a nested block (`restore_to_point_in_time`) is
102
+ * given. The provider reads these with `GetOk`, so a literal `""` is the same
103
+ * as leaving the argument out; anything else, a reference included, is given.
104
+ */
105
+ export function isGiven(body, name) {
106
+ const a = attr(body, name);
107
+ if (a.kind === "absent")
108
+ return false;
109
+ if (a.kind === "literal") {
110
+ if (a.value === "" || a.value === false)
111
+ return false;
112
+ if (Array.isArray(a.value))
113
+ return a.value.length > 0;
114
+ }
115
+ return true;
116
+ }
117
+ /** The bodies of a nested block, re-exported for the rule files. */
118
+ export { nestedBodies };
119
+ /** A finding the literal proves. */
120
+ export function finding(checkId, severity, block, message) {
121
+ return { checkId, severity, message: `"${block.address}" ${message}`, entity: block.key, lexicon: "terraform" };
122
+ }
123
+ /**
124
+ * The one `info` diagnostic a rule emits when it cannot read the value it
125
+ * needs. The message starts `Not determined:`, as #2284 fixed for TF030-TF037.
126
+ */
127
+ export function notDetermined(checkId, block, reason, tail) {
128
+ return {
129
+ checkId,
130
+ severity: "info",
131
+ message: `Not determined: "${block.address}" ${reason}. ${tail}`,
132
+ entity: block.key,
133
+ lexicon: "terraform",
134
+ };
135
+ }
@@ -0,0 +1,90 @@
1
+ import { tf001 } from "./tf001.js";
2
+ import { tf002 } from "./tf002.js";
3
+ import { tf003 } from "./tf003.js";
4
+ import { tf004 } from "./tf004.js";
5
+ import { tf005 } from "./tf005.js";
6
+ import { tf006 } from "./tf006.js";
7
+ import { tf007 } from "./tf007.js";
8
+ import { tf008 } from "./tf008.js";
9
+ import { tf009 } from "./tf009.js";
10
+ import { tf010 } from "./tf010.js";
11
+ import { tf011 } from "./tf011.js";
12
+ import { tf012 } from "./tf012.js";
13
+ import { tf013 } from "./tf013.js";
14
+ import { tf014 } from "./tf014.js";
15
+ import { tf015 } from "./tf015.js";
16
+ import { tf016 } from "./tf016.js";
17
+ import { tf017 } from "./tf017.js";
18
+ import { tf018 } from "./tf018.js";
19
+ import { tf019 } from "./tf019.js";
20
+ import { tf020 } from "./tf020.js";
21
+ import { tf021 } from "./tf021.js";
22
+ import { tf022 } from "./tf022.js";
23
+ import { tf024 } from "./tf024.js";
24
+ import { tf025 } from "./tf025.js";
25
+ import { tf026 } from "./tf026.js";
26
+ import { tf027 } from "./tf027.js";
27
+ import { tf028 } from "./tf028.js";
28
+ import { tf029 } from "./tf029.js";
29
+ import { tf030 } from "./tf030.js";
30
+ import { tf031 } from "./tf031.js";
31
+ import { tf032 } from "./tf032.js";
32
+ import { tf033 } from "./tf033.js";
33
+ import { tf034 } from "./tf034.js";
34
+ import { tf035 } from "./tf035.js";
35
+ import { tf036 } from "./tf036.js";
36
+ import { tf037 } from "./tf037.js";
37
+ import { tf038 } from "./tf038.js";
38
+ import { tf039 } from "./tf039.js";
39
+ import { tf040 } from "./tf040.js";
40
+ import { tf041 } from "./tf041.js";
41
+ import { tf042 } from "./tf042.js";
42
+ import { tf043 } from "./tf043.js";
43
+ import { tf044 } from "./tf044.js";
44
+ import { tf045 } from "./tf045.js";
45
+ export const postSynthChecks = [
46
+ tf001,
47
+ tf002,
48
+ tf003,
49
+ tf004,
50
+ tf005,
51
+ tf006,
52
+ tf007,
53
+ tf008,
54
+ tf009,
55
+ tf010,
56
+ tf011,
57
+ tf012,
58
+ tf013,
59
+ tf014,
60
+ tf015,
61
+ tf016,
62
+ tf017,
63
+ tf018,
64
+ tf019,
65
+ tf020,
66
+ tf021,
67
+ tf022,
68
+ tf024,
69
+ tf025,
70
+ tf026,
71
+ tf027,
72
+ tf028,
73
+ tf029,
74
+ tf030,
75
+ tf031,
76
+ tf032,
77
+ tf033,
78
+ tf034,
79
+ tf035,
80
+ tf036,
81
+ tf037,
82
+ tf038,
83
+ tf039,
84
+ tf040,
85
+ tf041,
86
+ tf042,
87
+ tf043,
88
+ tf044,
89
+ tf045,
90
+ ];
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Classify a `module` block's `source` string, shared by TF004 (registry
3
+ * module without a version) and TF005 (git/hg module unpinned or pinned to
4
+ * a branch). Neither rule evaluates HCL: both read `source` as the literal
5
+ * string hcl2json hands back, so a source built from a variable or a
6
+ * function call is never classified (`"other"`), matching tflint's own
7
+ * `SourceKnown` guard on `module.Source`.
8
+ *
9
+ * `registry` mirrors `hashicorp/terraform-registry-address`'s
10
+ * `ParseModuleSource` (MPL-2.0,
11
+ * https://github.com/hashicorp/terraform-registry-address/blob/main/module.go):
12
+ * three slash-separated parts (`namespace/name/target-system`), or four with
13
+ * a leading hostname that contains a dot and is not `github.com` or
14
+ * `bitbucket.org` (those are reserved for direct VCS installs, never a
15
+ * registry). `git` covers the sources tflint's `terraform_module_pinned_source`
16
+ * treats as git/hg: an explicit `git::`/`hg::` force prefix, the `github.com`
17
+ * and `bitbucket.org` shorthands its `GitHubDetector`/`BitBucketDetector`
18
+ * recognize, scp-style `git@host:path` addresses, and any source ending in
19
+ * `.git`. `local` is `./` or `../` (checkov's `UNKNOWN` case for module
20
+ * pinning: a local path cannot be pinned to anything and is never flagged).
21
+ * `oci` is an OpenTofu `oci://host/repo` source (#3190). OpenTofu selects a
22
+ * version with `?tag=` or `?digest=` (https://opentofu.org/docs/language/modules/sources/),
23
+ * and a source naming neither resolves the `latest` tag. The container-image
24
+ * shapes `repo:tag` and `repo@sha256:...` are read too, so a source written
25
+ * the way `docker pull` spells it is not misread as unpinned. A `:` inside the
26
+ * host (a registry port) is never a tag: only a colon in the last path
27
+ * segment is. The tag and digest rules were checked against the docs page
28
+ * above; the suffix forms are not documented there and are not verified
29
+ * against OpenTofu's parser.
30
+ */
31
+ const REGISTRY_NAME = /^[0-9A-Za-z](?:[0-9A-Za-z_-]{0,62}[0-9A-Za-z])?$/;
32
+ const REGISTRY_TARGET_SYSTEM = /^[0-9a-z]{1,64}$/;
33
+ const RESERVED_VCS_HOSTS = new Set(["github.com", "bitbucket.org"]);
34
+ /** Split a raw query string (no leading `?`) into its key/value pairs, last write wins. */
35
+ function parseQuery(query) {
36
+ const params = new Map();
37
+ for (const pair of query.split("&")) {
38
+ if (pair === "")
39
+ continue;
40
+ const eq = pair.indexOf("=");
41
+ const key = eq === -1 ? pair : pair.slice(0, eq);
42
+ const value = eq === -1 ? "" : decodeURIComponent(pair.slice(eq + 1));
43
+ params.set(decodeURIComponent(key), value);
44
+ }
45
+ return params;
46
+ }
47
+ /** Strip a `//<subdir>` package-subdirectory suffix, ignoring one embedded in the scheme (`https://`). */
48
+ function stripSubdir(base) {
49
+ const schemeEnd = base.indexOf("://");
50
+ const searchFrom = schemeEnd === -1 ? 0 : schemeEnd + 3;
51
+ const idx = base.indexOf("//", searchFrom);
52
+ return idx === -1 ? base : base.slice(0, idx);
53
+ }
54
+ function isRegistryAddress(rest) {
55
+ const parts = stripSubdir(rest).split("/");
56
+ let namespace, name, targetSystem;
57
+ if (parts.length === 3) {
58
+ [namespace, name, targetSystem] = parts;
59
+ }
60
+ else if (parts.length === 4) {
61
+ const host = parts[0];
62
+ if (!host.includes(".") || RESERVED_VCS_HOSTS.has(host.toLowerCase()))
63
+ return false;
64
+ [, namespace, name, targetSystem] = parts;
65
+ }
66
+ else {
67
+ return false;
68
+ }
69
+ return REGISTRY_NAME.test(namespace) && REGISTRY_NAME.test(name) && REGISTRY_TARGET_SYSTEM.test(targetSystem);
70
+ }
71
+ /** Is `rest` (source with any `git::`/`hg::` force prefix already stripped) a git/hg-shaped source? */
72
+ function isGitShaped(rest) {
73
+ if (/^[\w.-]+@[^:/]+:/.test(rest))
74
+ return true; // scp-style, e.g. git@github.com:org/repo.git
75
+ const withoutScheme = rest.replace(/^[a-z][a-z0-9+.-]*:\/\//i, "");
76
+ if (/^(github\.com|bitbucket\.org)\//i.test(withoutScheme))
77
+ return true;
78
+ if (/\.git($|[/?])/i.test(withoutScheme))
79
+ return true;
80
+ return false;
81
+ }
82
+ function classifyOci(base, query) {
83
+ const params = parseQuery(query);
84
+ let path = stripSubdir(base).replace(/^oci:\/\//i, "");
85
+ let tag = params.get("tag") || undefined;
86
+ let digest = params.get("digest") || undefined;
87
+ const at = path.indexOf("@");
88
+ if (at !== -1) {
89
+ digest = digest ?? (path.slice(at + 1) || undefined);
90
+ path = path.slice(0, at);
91
+ }
92
+ const lastSlash = path.lastIndexOf("/");
93
+ const colon = path.indexOf(":", lastSlash + 1); // a port sits before the first slash
94
+ if (colon !== -1) {
95
+ tag = tag ?? (path.slice(colon + 1) || undefined);
96
+ }
97
+ return { kind: "oci", ...(tag !== undefined ? { tag } : {}), ...(digest !== undefined ? { digest } : {}) };
98
+ }
99
+ export function classifyModuleSource(source) {
100
+ const trimmed = source.trim();
101
+ if (trimmed.startsWith("./") || trimmed.startsWith("../") || trimmed.startsWith(".\\") || trimmed.startsWith("..\\")) {
102
+ return { kind: "local" };
103
+ }
104
+ const queryIdx = trimmed.indexOf("?");
105
+ const base = queryIdx === -1 ? trimmed : trimmed.slice(0, queryIdx);
106
+ const query = queryIdx === -1 ? "" : trimmed.slice(queryIdx + 1);
107
+ if (/^oci:\/\//i.test(base))
108
+ return classifyOci(base, query);
109
+ const forced = /^(git|hg)::/i.exec(base);
110
+ const rest = forced ? base.slice(forced[0].length) : base;
111
+ if (forced || isGitShaped(rest)) {
112
+ const params = parseQuery(query);
113
+ const ref = params.get("ref") || params.get("rev") || undefined;
114
+ return { kind: "git", ref };
115
+ }
116
+ if (isRegistryAddress(rest))
117
+ return { kind: "registry" };
118
+ return { kind: "other" };
119
+ }
120
+ const TAG_LIKE = /^v?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/;
121
+ const FULL_SHA = /^[0-9a-f]{40}$/i;
122
+ const DEFAULT_BRANCHES = new Set(["main", "master", "develop", "trunk"]);
123
+ /**
124
+ * Is `ref` pinned tightly enough for TF005: a semver-shaped tag or a full
125
+ * 40-hex commit SHA. Stricter than checkov's `CKV_TF_1`/`CKV_TF_2`, which
126
+ * accept any `?ref=` containing `\d\.\d` (so `v1.2-dev` passes there); the
127
+ * page documents the difference.
128
+ */
129
+ export function isPinnedRef(ref) {
130
+ return TAG_LIKE.test(ref) || FULL_SHA.test(ref);
131
+ }
132
+ /** Is `ref` one of the well-known mutable branch names tflint's `flexible` style rejects, plus `trunk`. */
133
+ export function isDefaultBranch(ref) {
134
+ return DEFAULT_BRANCHES.has(ref);
135
+ }
136
+ const MUTABLE_TAGS = new Set(["latest", "main", "master", "develop", "dev", "stable", "edge", "nightly"]);
137
+ /**
138
+ * Is `tag` a name that conventionally moves (`latest`, a branch-like name)
139
+ * rather than an exact version. Used by TF038. A tag is a mutable pointer in
140
+ * most registries whatever it looks like, so the rule is a warning and this
141
+ * only decides what reads as obviously floating.
142
+ */
143
+ export function isMutableOciTag(tag) {
144
+ return MUTABLE_TAGS.has(tag.toLowerCase());
145
+ }
146
+ /**
147
+ * Is a registry `version` constraint an exact version (`1.4.0`, `= 1.4.0`,
148
+ * `=1.4.0`) rather than a range (`~> 1.4`, `>= 1.4`, `>= 1.0, < 2.0`). Used
149
+ * by TF039. A bare version is an exact constraint in Terraform.
150
+ */
151
+ export function isExactVersionConstraint(version) {
152
+ return /^(?:=\s*)?v?\d+(?:\.\d+){0,2}(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(version.trim()) && !version.includes(",");
153
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Root scope or child scope, and the `Callers:` chain that says which call
3
+ * reached a finding (chant #2112).
4
+ *
5
+ * Since the parse descends into local modules, `ctx.entities` holds two kinds
6
+ * of block: the root module's own, keyed `<root>/<address>`, and a child
7
+ * module's, keyed `<root>/module.<name>/<address>` with the call chain on
8
+ * `props.callers`. Every check has to say which it applies to, and the three
9
+ * shapes that answer live here:
10
+ *
11
+ * - {@link isRootScoped} for a check about how a ROOT is assembled (TF001's
12
+ * backend, TF002's `required_providers`): a child module has no backend and
13
+ * no provider requirements of its own to judge, and reading one as if it
14
+ * were the root's is how a root with a perfectly good backend gets flagged
15
+ * because a module beneath it has none.
16
+ * - {@link isChildScoped} for the mirror, the two rules that are ONLY about
17
+ * child modules (TF014, TF015).
18
+ * - {@link callersNote} for the sentence appended to a finding inside a
19
+ * child, naming each call site with the file and line it sits on. tflint
20
+ * prints the same thing as a `Callers:` block under the diagnostic
21
+ * (https://github.com/terraform-linters/tflint/blob/master/docs/user-guide/calling-modules.md);
22
+ * a `PostSynthDiagnostic` carries no ranges, so the chain goes in the
23
+ * message, where a reader of any output format sees it.
24
+ *
25
+ * A check with no opinion (most of them: an unset `type` on a variable is the
26
+ * same finding wherever the variable lives) needs nothing from this module and
27
+ * runs over both scopes, which is what tflint does with its own language rules
28
+ * under `--call-module-type=local`.
29
+ */
30
+ import { isResourceDeclarable } from "@intentius/chant/declarable";
31
+ import { callersOfKey } from "../../hcl/parse.js";
32
+ /** The `module.<name>` chain on an entity, outermost first. Empty for a root block. */
33
+ export function callersOf(entity) {
34
+ if (!isResourceDeclarable(entity))
35
+ return [];
36
+ const props = entity.props;
37
+ return Array.isArray(props.callers) ? props.callers : [];
38
+ }
39
+ /** Was this block declared in the root module itself? */
40
+ export function isRootScoped(entity) {
41
+ return callersOf(entity).length === 0;
42
+ }
43
+ /** Was this block declared in a child module the root calls? */
44
+ export function isChildScoped(entity) {
45
+ return callersOf(entity).length > 0;
46
+ }
47
+ /**
48
+ * The `Callers:` sentence for a finding on `key`, or `""` when the finding is
49
+ * in the root module.
50
+ *
51
+ * The call sites are the key's own prefixes: a finding on
52
+ * `app/module.cdn/module.bucket/aws_s3_bucket.assets` was reached through
53
+ * `app/module.cdn` and then `app/module.cdn/module.bucket`, both of them
54
+ * `module` block entities in `ctx.entities`, so the file and line each was
55
+ * parsed from are already recorded. A prefix with no entity (a hand-built
56
+ * context in a test) renders as the bare call name.
57
+ */
58
+ export function callersNote(ctx, key) {
59
+ const callers = callersOfKey(key);
60
+ if (callers.length === 0)
61
+ return "";
62
+ const root = key.slice(0, key.indexOf("/"));
63
+ const hops = callers.map((caller, i) => {
64
+ const callSite = [root, ...callers.slice(0, i + 1)].join("/");
65
+ const entity = ctx.entities.get(callSite);
66
+ if (!entity || !isResourceDeclarable(entity))
67
+ return caller;
68
+ const props = entity.props;
69
+ const file = typeof props.file === "string" ? props.file : "";
70
+ if (file === "")
71
+ return caller;
72
+ return typeof props.line === "number" ? `${caller} (${file}:${props.line})` : `${caller} (${file})`;
73
+ });
74
+ return ` Callers: ${[root, ...hops].join(" -> ")}.`;
75
+ }
76
+ /**
77
+ * Wrap every check so a finding inside a child module carries its
78
+ * {@link callersNote}, whichever rule produced it.
79
+ *
80
+ * Done once, here, rather than in each rule's message: a reader of a finding
81
+ * on `module.cdn`'s bucket needs to know which call reached that code no
82
+ * matter which of the twenty-odd checks reported it, and a rule that forgot
83
+ * to append the chain would be a silent gap. The wrapper is what
84
+ * `terraformPlugin.postSynthChecks()` returns; a rule's own `check()` is
85
+ * unchanged, which is what its unit tests call.
86
+ *
87
+ * A message that already names the chain is left alone, so a rule with
88
+ * something more specific to say about its callers can say it.
89
+ */
90
+ export function withCallersChain(checks) {
91
+ return checks.map((check) => ({
92
+ ...check,
93
+ check(ctx) {
94
+ return check.check(ctx).map((diagnostic) => {
95
+ if (typeof diagnostic.entity !== "string")
96
+ return diagnostic;
97
+ if (diagnostic.message.includes("Callers:"))
98
+ return diagnostic;
99
+ const note = callersNote(ctx, diagnostic.entity);
100
+ return note === "" ? diagnostic : { ...diagnostic, message: diagnostic.message + note };
101
+ });
102
+ },
103
+ }));
104
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Shared reading for the Terragrunt rules TF041 to TF045 (#3417).
3
+ *
4
+ * These rules look at `terragrunt.hcl` and `root.hcl`, which the parse reads
5
+ * into their own entity types (`TERRAGRUNT_*_TYPE` in `../../hcl/parse.ts`)
6
+ * and never into the module types the other TF rules check. Like the module
7
+ * source rules, they read the literal values hcl2json hands back: an
8
+ * attribute built from a function or a `local.` reference arrives as a
9
+ * `"${...}"` string and is treated as not determined, never as a finding.
10
+ */
11
+ import { isResourceDeclarable } from "@intentius/chant/declarable";
12
+ import { classifyModuleSource } from "./module-source.js";
13
+ /** Entities of one Terragrunt type, with their keys, in map order. */
14
+ export function terragruntEntities(entities, entityType) {
15
+ const out = [];
16
+ for (const [key, entity] of entities) {
17
+ if (entity.entityType !== entityType || !isResourceDeclarable(entity))
18
+ continue;
19
+ const props = entity.props;
20
+ out.push({
21
+ key,
22
+ address: typeof props.address === "string" ? props.address : key,
23
+ file: typeof props.file === "string" ? props.file : "terragrunt.hcl",
24
+ body: (typeof props.body === "object" && props.body !== null ? props.body : {}),
25
+ });
26
+ }
27
+ return out;
28
+ }
29
+ /** An HCL expression hcl2json left as `"${...}"`: its value is not known without evaluating it. */
30
+ export function isExpression(value) {
31
+ return typeof value === "string" && value.includes("${");
32
+ }
33
+ /**
34
+ * Classify a unit's `terraform.source`. Git and `oci://` sources go through
35
+ * {@link classifyModuleSource}, the reader TF005 and TF038 use. `tfr://` is
36
+ * Terragrunt's own scheme for registry modules and takes its version from the
37
+ * `?version=` query argument. A source that holds an interpolation, or that
38
+ * names an absolute path or an archive URL, is `other` or `local` and is
39
+ * never reported.
40
+ */
41
+ export function classifyTerragruntSource(source) {
42
+ const trimmed = source.trim();
43
+ if (trimmed.includes("${"))
44
+ return { kind: "other" };
45
+ if (/^tfr:\/\//i.test(trimmed)) {
46
+ const query = trimmed.includes("?") ? trimmed.slice(trimmed.indexOf("?") + 1) : "";
47
+ const version = new URLSearchParams(query).get("version");
48
+ return { kind: "tfr", pinned: version !== null && version.trim() !== "" };
49
+ }
50
+ if (trimmed.startsWith("/"))
51
+ return { kind: "local" };
52
+ const classified = classifyModuleSource(trimmed);
53
+ switch (classified.kind) {
54
+ case "local":
55
+ return { kind: "local" };
56
+ case "git":
57
+ return { kind: "git", pinned: classified.ref !== undefined };
58
+ case "oci":
59
+ return { kind: "oci", pinned: classified.tag !== undefined || classified.digest !== undefined };
60
+ default:
61
+ return { kind: "other" };
62
+ }
63
+ }