@intentius/chant-lexicon-aws 0.46.0 → 0.49.0

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 (114) hide show
  1. package/dist/api/read-client.d.ts +10 -2
  2. package/dist/api/read-client.d.ts.map +1 -1
  3. package/dist/composites/agentcore-agent.d.ts +26 -1
  4. package/dist/composites/agentcore-agent.d.ts.map +1 -1
  5. package/dist/composites/bucket-deployment.d.ts +82 -0
  6. package/dist/composites/bucket-deployment.d.ts.map +1 -0
  7. package/dist/composites/dynamodb-table.d.ts +45 -0
  8. package/dist/composites/dynamodb-table.d.ts.map +1 -0
  9. package/dist/composites/ec2-instance-bundle.d.ts +61 -0
  10. package/dist/composites/ec2-instance-bundle.d.ts.map +1 -0
  11. package/dist/composites/ecr-repository.d.ts +49 -0
  12. package/dist/composites/ecr-repository.d.ts.map +1 -0
  13. package/dist/composites/fargate-service.d.ts +16 -0
  14. package/dist/composites/fargate-service.d.ts.map +1 -1
  15. package/dist/composites/index.d.ts +16 -2
  16. package/dist/composites/index.d.ts.map +1 -1
  17. package/dist/composites/monitoring-stack.d.ts +99 -0
  18. package/dist/composites/monitoring-stack.d.ts.map +1 -0
  19. package/dist/composites/nlb-service.d.ts +48 -0
  20. package/dist/composites/nlb-service.d.ts.map +1 -0
  21. package/dist/composites/step-functions-workflow.d.ts +36 -0
  22. package/dist/composites/step-functions-workflow.d.ts.map +1 -0
  23. package/dist/deep-observe.d.ts +45 -0
  24. package/dist/deep-observe.d.ts.map +1 -1
  25. package/dist/deep-topology.d.ts +104 -0
  26. package/dist/deep-topology.d.ts.map +1 -0
  27. package/dist/effect-receipt-row.d.ts +92 -0
  28. package/dist/effect-receipt-row.d.ts.map +1 -0
  29. package/dist/index.d.ts +6 -2
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/integrity.json +7 -4
  32. package/dist/lint/audit-catalog.d.ts.map +1 -1
  33. package/dist/lint/post-synth/cf-refs.d.ts +17 -0
  34. package/dist/lint/post-synth/cf-refs.d.ts.map +1 -1
  35. package/dist/lint/post-synth/index.d.ts.map +1 -1
  36. package/dist/lint/post-synth/waw061.d.ts +19 -0
  37. package/dist/lint/post-synth/waw061.d.ts.map +1 -0
  38. package/dist/lint/post-synth/waw062.d.ts +19 -0
  39. package/dist/lint/post-synth/waw062.d.ts.map +1 -0
  40. package/dist/lint/post-synth/waw063.d.ts +35 -0
  41. package/dist/lint/post-synth/waw063.d.ts.map +1 -0
  42. package/dist/manifest.json +1 -1
  43. package/dist/okf/index.md +3 -0
  44. package/dist/okf/rules/WAW061.md +17 -0
  45. package/dist/okf/rules/WAW062.md +27 -0
  46. package/dist/okf/rules/WAW063.md +19 -0
  47. package/dist/okf/types/Action.md +1 -0
  48. package/dist/okf/types/AutoScalingGroup.md +1 -0
  49. package/dist/okf/types/Bucket.md +1 -0
  50. package/dist/okf/types/ECRRepository.md +1 -0
  51. package/dist/okf/types/Function.md +1 -0
  52. package/dist/okf/types/IamPolicy.md +1 -0
  53. package/dist/okf/types/LaunchTemplate.md +1 -0
  54. package/dist/okf/types/LoadBalancer.md +1 -0
  55. package/dist/okf/types/ManagedPolicy.md +2 -0
  56. package/dist/okf/types/Map.md +1 -0
  57. package/dist/okf/types/Role.md +2 -0
  58. package/dist/okf/types/SecurityGroup.md +1 -0
  59. package/dist/okf/types/StateMachine.md +4 -0
  60. package/dist/okf/types/Subnet.md +4 -0
  61. package/dist/okf/types/Table.md +1 -0
  62. package/dist/okf/types/Type.md +3 -0
  63. package/dist/okf/types/Vpc.md +1 -0
  64. package/dist/op/activities/index.d.ts +6 -0
  65. package/dist/op/activities/index.d.ts.map +1 -1
  66. package/dist/plugin.d.ts.map +1 -1
  67. package/dist/receipt-store.d.ts +109 -0
  68. package/dist/receipt-store.d.ts.map +1 -0
  69. package/dist/rules/cf-refs.ts +40 -0
  70. package/dist/rules/waw061.ts +67 -0
  71. package/dist/rules/waw062.ts +0 -0
  72. package/dist/rules/waw063.ts +0 -0
  73. package/dist/serializer.d.ts.map +1 -1
  74. package/dist/skills/chant-aws-carve-terraform.md +26 -23
  75. package/package.json +3 -3
  76. package/src/api/read-client.test.ts +9 -0
  77. package/src/api/read-client.ts +15 -2
  78. package/src/composites/agentcore-agent.ts +35 -13
  79. package/src/composites/bucket-deployment.test.ts +84 -0
  80. package/src/composites/bucket-deployment.ts +153 -0
  81. package/src/composites/composites.test.ts +325 -0
  82. package/src/composites/dynamodb-table.test.ts +154 -0
  83. package/src/composites/dynamodb-table.ts +124 -0
  84. package/src/composites/ec2-instance-bundle.ts +134 -0
  85. package/src/composites/ecr-repository.test.ts +118 -0
  86. package/src/composites/ecr-repository.ts +117 -0
  87. package/src/composites/fargate-service.ts +19 -3
  88. package/src/composites/index.ts +21 -2
  89. package/src/composites/monitoring-stack.test.ts +130 -0
  90. package/src/composites/monitoring-stack.ts +164 -0
  91. package/src/composites/nlb-service.ts +117 -0
  92. package/src/composites/step-functions-workflow.ts +127 -0
  93. package/src/deep-observe.test.ts +201 -1
  94. package/src/deep-observe.ts +204 -0
  95. package/src/deep-topology.test.ts +392 -0
  96. package/src/deep-topology.ts +237 -0
  97. package/src/effect-receipt-row.test.ts +164 -0
  98. package/src/effect-receipt-row.ts +147 -0
  99. package/src/index.ts +28 -7
  100. package/src/lint/audit-catalog.ts +6 -0
  101. package/src/lint/post-synth/cf-refs.ts +40 -0
  102. package/src/lint/post-synth/index.ts +6 -0
  103. package/src/lint/post-synth/waw061.test.ts +99 -0
  104. package/src/lint/post-synth/waw061.ts +67 -0
  105. package/src/lint/post-synth/waw062.test.ts +109 -0
  106. package/src/lint/post-synth/waw062.ts +0 -0
  107. package/src/lint/post-synth/waw063.test.ts +220 -0
  108. package/src/lint/post-synth/waw063.ts +0 -0
  109. package/src/op/activities/index.ts +15 -0
  110. package/src/plugin.ts +41 -10
  111. package/src/receipt-store.test.ts +204 -0
  112. package/src/receipt-store.ts +315 -0
  113. package/src/serializer.ts +105 -2
  114. package/src/skills/chant-aws-carve-terraform.md +26 -23
@@ -0,0 +1,164 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import {
3
+ EffectReceipt,
4
+ receiptParameterName,
5
+ AWS_EFFECT_RECEIPT_ENTITY_TYPE,
6
+ EFFECT_RECEIPTS_METADATA_KEY,
7
+ RECEIPT_UNRESOLVED_VALUE_NOTE,
8
+ } from "./effect-receipt-row";
9
+ import { awsSerializer } from "./serializer";
10
+ import { AWS_TAG_OWNERSHIP_KEYS } from "./ownership";
11
+ import {
12
+ EXISTENCE_EXPECTATION,
13
+ isEffectReceipt,
14
+ receiptExpectation,
15
+ EFFECT_RECEIPT_MARKER,
16
+ } from "@intentius/chant/effect-receipt";
17
+ import { receiptCheckInput } from "@intentius/chant/op/receipt-store";
18
+ import { coreReceiptChecks, RECEIPT_PLAIN_STORE_CHECK_ID } from "@intentius/chant/lint/receipt-checks";
19
+ import { DECLARABLE_MARKER, type Declarable } from "@intentius/chant/declarable";
20
+ import { INTRINSIC_MARKER } from "@intentius/chant/intrinsic";
21
+ import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
22
+
23
+ /** A minimal deploy-time reference, for the placeholder-value case. */
24
+ const someRef = { [INTRINSIC_MARKER]: true as const, toJSON: () => ({ Ref: "other" }) };
25
+
26
+ function serializeReceipts(
27
+ receipts: Map<string, Declarable>,
28
+ ownership?: { stack: string; env?: string },
29
+ ): { Metadata?: Record<string, unknown>; Resources: Record<string, unknown> } {
30
+ const out = awsSerializer.serialize(new Map(), [], {
31
+ ...(ownership ? { ownership } : {}),
32
+ receipts,
33
+ });
34
+ return JSON.parse(typeof out === "string" ? out : out.primary);
35
+ }
36
+
37
+ function metadataRows(template: { Metadata?: Record<string, unknown> }) {
38
+ return template.Metadata?.[EFFECT_RECEIPTS_METADATA_KEY] as Record<
39
+ string,
40
+ { Type: string; Properties: Record<string, unknown> }
41
+ >;
42
+ }
43
+
44
+ describe("EffectReceipt (aws materialization row)", () => {
45
+ it("declares under the aws lexicon with the real resource kind, carrying the marker", () => {
46
+ const r = EffectReceipt("seeded", { effect: "db-seed", flavor: "existence" });
47
+ expect(r.lexicon).toBe("aws");
48
+ expect(r.entityType).toBe(AWS_EFFECT_RECEIPT_ENTITY_TYPE);
49
+ expect(isEffectReceipt(r)).toBe(true);
50
+ expect(r.props.Type).toBe("String");
51
+ });
52
+
53
+ it("is accepted by the effect() step's receiptCheckInput, expectation stamped when static", () => {
54
+ const r = EffectReceipt("seeded", { effect: "db-seed", flavor: "hash", inputs: { v: 1 } });
55
+ const input = receiptCheckInput(r);
56
+ expect(input.receipt.effect).toBe("db-seed");
57
+ expect(input.expectation).toBe(receiptExpectation(r));
58
+ });
59
+
60
+ it("validates the effect as a path segment at declaration", () => {
61
+ expect(() => EffectReceipt("bad", { effect: "a/b", flavor: "existence" })).toThrow(/path segment/);
62
+ expect(() => EffectReceipt("bad", { effect: "", flavor: "existence" })).toThrow(/non-empty/);
63
+ });
64
+ });
65
+
66
+ describe("receiptParameterName", () => {
67
+ it("derives /chant-receipts/<stack>/<env>/<effect>", () => {
68
+ expect(receiptParameterName("demo", "dev", "db-seed")).toBe("/chant-receipts/demo/dev/db-seed");
69
+ });
70
+
71
+ it("refuses a segment that would deepen the hierarchy", () => {
72
+ expect(() => receiptParameterName("a/b", "dev", "seed")).toThrow(/stack/);
73
+ expect(() => receiptParameterName("demo", "", "seed")).toThrow(/env/);
74
+ });
75
+ });
76
+
77
+ describe("awsSerializer receipt rows", () => {
78
+ const ownership = { stack: "demo", env: "dev" };
79
+
80
+ it("renders each receipt as a plain-String SSM parameter row at the derived path, ownership-tagged", () => {
81
+ const seeded = EffectReceipt("seeded", { effect: "db-seed", flavor: "hash", inputs: { v: 1 } });
82
+ const template = serializeReceipts(new Map([["seeded", seeded]]), ownership);
83
+ const row = metadataRows(template).seeded;
84
+ expect(row.Type).toBe("AWS::SSM::Parameter");
85
+ expect(row.Properties.Name).toBe("/chant-receipts/demo/dev/db-seed");
86
+ expect(row.Properties.Type).toBe("String");
87
+ expect(row.Properties.Value).toBe(receiptExpectation(seeded));
88
+ expect(row.Properties.Tags).toContainEqual({ Key: AWS_TAG_OWNERSHIP_KEYS.managedBy, Value: "chant" });
89
+ expect(row.Properties.Tags).toContainEqual({ Key: AWS_TAG_OWNERSHIP_KEYS.stack, Value: "demo" });
90
+ expect(row.Properties.Tags).toContainEqual({ Key: AWS_TAG_OWNERSHIP_KEYS.env, Value: "dev" });
91
+ });
92
+
93
+ it("keeps the receipt out of Resources — the section appliers write from", () => {
94
+ const seeded = EffectReceipt("seeded", { effect: "db-seed", flavor: "existence" });
95
+ const template = serializeReceipts(new Map([["seeded", seeded]]), ownership);
96
+ expect(Object.keys(template.Resources)).toHaveLength(0);
97
+ expect(metadataRows(template).seeded).toBeDefined();
98
+ });
99
+
100
+ it("renders the existence expectation for an existence receipt", () => {
101
+ const r = EffectReceipt("booted", { effect: "bootstrap", flavor: "existence" });
102
+ const template = serializeReceipts(new Map([["booted", r]]), ownership);
103
+ expect(metadataRows(template).booted.Properties.Value).toBe(EXISTENCE_EXPECTATION);
104
+ });
105
+
106
+ it("renders the placeholder note, never a placeholder digest, for reference inputs", () => {
107
+ const r = EffectReceipt("wired", { effect: "wire-up", flavor: "hash", inputs: { target: someRef } });
108
+ const template = serializeReceipts(new Map([["wired", r]]), ownership);
109
+ expect(metadataRows(template).wired.Properties.Value).toBe(RECEIPT_UNRESOLVED_VALUE_NOTE);
110
+ });
111
+
112
+ it("errors when no ownership marker resolves", () => {
113
+ const r = EffectReceipt("seeded", { effect: "db-seed", flavor: "existence" });
114
+ expect(() => serializeReceipts(new Map([["seeded", r]]))).toThrow(/ownership/);
115
+ });
116
+
117
+ it("errors when ownership resolves no env — the segment is explicit, never guessed", () => {
118
+ const r = EffectReceipt("seeded", { effect: "db-seed", flavor: "existence" });
119
+ expect(() => serializeReceipts(new Map([["seeded", r]]), { stack: "demo" })).toThrow(/env/);
120
+ });
121
+
122
+ it("emits no receipt block when the context carries no receipts", () => {
123
+ const out = awsSerializer.serialize(new Map(), [], { ownership });
124
+ const template = JSON.parse(typeof out === "string" ? out : out.primary);
125
+ expect(template.Metadata?.[EFFECT_RECEIPTS_METADATA_KEY]).toBeUndefined();
126
+ });
127
+ });
128
+
129
+ describe("#1833's plain-store guard over the aws row", () => {
130
+ const check = coreReceiptChecks().find((c) => c.id === RECEIPT_PLAIN_STORE_CHECK_ID)!;
131
+
132
+ function runCheck(entities: Map<string, Declarable>) {
133
+ const ctx: PostSynthContext = {
134
+ outputs: new Map(),
135
+ entities,
136
+ buildResult: { outputs: new Map(), entities, warnings: [], errors: [], sourceFileCount: 0 },
137
+ };
138
+ return check.check(ctx);
139
+ }
140
+
141
+ it("passes the factory's row — SSM plain String", () => {
142
+ const r = EffectReceipt("seeded", { effect: "db-seed", flavor: "existence" });
143
+ expect(runCheck(new Map([["seeded", r]]))).toEqual([]);
144
+ });
145
+
146
+ it("fails a SecureString fixture", () => {
147
+ const secure = {
148
+ [DECLARABLE_MARKER]: true as const,
149
+ [EFFECT_RECEIPT_MARKER]: true as const,
150
+ lexicon: "aws",
151
+ entityType: "AWS::SSM::Parameter",
152
+ name: "sneaky",
153
+ effect: "db-seed",
154
+ flavor: "existence" as const,
155
+ inputs: {},
156
+ props: { Type: "SecureString" },
157
+ };
158
+ const diagnostics = runCheck(new Map([["sneaky", secure as unknown as Declarable]]));
159
+ expect(diagnostics).toHaveLength(1);
160
+ expect(diagnostics[0].checkId).toBe(RECEIPT_PLAIN_STORE_CHECK_ID);
161
+ expect(diagnostics[0].severity).toBe("error");
162
+ expect(diagnostics[0].message).toMatch(/SecureString/);
163
+ });
164
+ });
@@ -0,0 +1,147 @@
1
+ /**
2
+ * The aws effect-receipt materialization row (#1835, epic #1703): an effect
3
+ * receipt stored as an `AWS::SSM::Parameter`, plain `String`, at
4
+ * `/chant-receipts/<stack>/<env>/<effect>`.
5
+ *
6
+ * Core's `EffectReceipt` factory (#1831) declares a receipt under the `chant`
7
+ * pseudo-lexicon, which no serializer claims — the designed shape until a
8
+ * lexicon materializes it. This module is that materialization for aws: the
9
+ * {@link EffectReceipt} factory here produces the same declaration shape but
10
+ * under `lexicon: "aws"`, so the build partitions it to the aws serializer,
11
+ * #1832's write-exclusion seam withholds it from the apply-bound entity set,
12
+ * and the serializer renders it for visibility through
13
+ * `SerializeContext.receipts` (see ./serializer.ts).
14
+ *
15
+ * Path identity (epic decision 4): the parameter name derives from the SAME
16
+ * ownership-block fields that stamp markers — `ownership.stack` and an
17
+ * explicit `ownership.env` — plus the receipt's `effect`. One derivation,
18
+ * {@link receiptParameterName}, shared by the serializer (the rendered row),
19
+ * the receipt store (../receipt-store.ts — what `receiptRead`/`receiptWrite`
20
+ * actually touch), and the observation leg (plan's live read). No env, no
21
+ * path: the callers error rather than guessing a segment.
22
+ *
23
+ * Plain store (epic decision 7, #1833's COR023): the parameter type is pinned
24
+ * to `String` at the source — the factory does not take a type, and the
25
+ * declared `props.Type` is the literal `"String"`. A receipt value is a
26
+ * witness (an existence marker or a `sha256:` digest), never a secret; a
27
+ * `SecureString` invites masking and rotation semantics that defeat the
28
+ * observe-and-compare loop, and the lint guard fails it.
29
+ */
30
+
31
+ import { DECLARABLE_MARKER, type Declarable } from "@intentius/chant/declarable";
32
+ import {
33
+ EffectReceipt as CoreEffectReceipt,
34
+ EFFECT_RECEIPT_MARKER,
35
+ type EffectReceiptFlavor,
36
+ type EffectReceiptOptions,
37
+ } from "@intentius/chant/effect-receipt";
38
+
39
+ /** The entityType of the aws materialization row — the real resource kind the
40
+ * receipt is stored as, which is what lint's plain-store guard checks. */
41
+ export const AWS_EFFECT_RECEIPT_ENTITY_TYPE = "AWS::SSM::Parameter";
42
+
43
+ /** Template-level `Metadata` key the serializer renders receipt rows under.
44
+ * Beside `chant:ownership` (./ownership.ts) and like it deliberately outside
45
+ * `Resources` — the applier writes from `Resources`, and a receipt must never
46
+ * enter the apply-bound document (#1832; the `effect()` step is the sole
47
+ * writer, epic decision 3). */
48
+ export const EFFECT_RECEIPTS_METADATA_KEY = "chant:effect-receipts";
49
+
50
+ /** Every receipt parameter lives under this prefix, so read-only IAM can be
51
+ * scoped to `arn:…:parameter/chant-receipts/*` and nothing else. */
52
+ export const RECEIPT_PATH_PREFIX = "/chant-receipts";
53
+
54
+ /**
55
+ * The rendered `Value` of a hash-flavor receipt that still carries reference
56
+ * inputs at synthesis. References resolve at plan and at run, never at
57
+ * synthesis (epic decision 5) — so the row carries this note instead of a
58
+ * digest hashed over placeholders.
59
+ */
60
+ export const RECEIPT_UNRESOLVED_VALUE_NOTE =
61
+ "unresolved at synthesis — reference inputs; the expectation resolves at plan and at run (chant #1703, decision 5)";
62
+
63
+ /** One path segment of the receipt parameter name. SSM hierarchical names are
64
+ * `/`-separated `[\w.-]+` segments, and the identity must stay one segment per
65
+ * field — a `/` inside a stack name would silently deepen the hierarchy. */
66
+ const PATH_SEGMENT = /^[\w.-]+$/;
67
+
68
+ function checkSegment(field: string, value: string): string {
69
+ if (!PATH_SEGMENT.test(value)) {
70
+ throw new Error(
71
+ `receipt path: ${field} "${value}" is not a valid SSM path segment — ` +
72
+ `use only letters, digits, ".", "_", "-" (the segment becomes one level of ` +
73
+ `${RECEIPT_PATH_PREFIX}/<stack>/<env>/<effect>)`,
74
+ );
75
+ }
76
+ return value;
77
+ }
78
+
79
+ /**
80
+ * The SSM parameter name of one effect's receipt:
81
+ * `/chant-receipts/<stack>/<env>/<effect>`, from the resolved ownership
82
+ * marker fields (epic decision 4). The single source of the path identity —
83
+ * the serializer's rendered row, the receipt store's reads and writes, and
84
+ * the observation leg all call this.
85
+ */
86
+ export function receiptParameterName(stack: string, env: string, effect: string): string {
87
+ return `${RECEIPT_PATH_PREFIX}/${checkSegment("stack", stack)}/${checkSegment("env", env)}/${checkSegment("effect", effect)}`;
88
+ }
89
+
90
+ /**
91
+ * An aws-materialized effect receipt: core's declaration shape (so
92
+ * `isEffectReceipt`, `effect(...)`, lint, and the plan engine all recognize it
93
+ * through the marker), under the aws lexicon and the real resource kind, with
94
+ * the store variant pinned plain.
95
+ */
96
+ export interface AwsEffectReceiptDeclaration extends Declarable {
97
+ readonly [EFFECT_RECEIPT_MARKER]: true;
98
+ readonly lexicon: "aws";
99
+ readonly entityType: typeof AWS_EFFECT_RECEIPT_ENTITY_TYPE;
100
+ /** The receipt's own name (the export-level identity of the witness). */
101
+ readonly name: string;
102
+ /** The effect this receipt witnesses — the path's final segment. */
103
+ readonly effect: string;
104
+ /** How the receipt is compared: mere presence, or a digest of the inputs. */
105
+ readonly flavor: EffectReceiptFlavor;
106
+ /** The effect's inputs as recorded at synthesis (references as placeholders). */
107
+ readonly inputs: Readonly<Record<string, unknown>>;
108
+ /** The declared parameter variant — pinned `String` at the source, which is
109
+ * the concrete half of #1833's plain-store guard. */
110
+ readonly props: { readonly Type: "String" };
111
+ }
112
+
113
+ /**
114
+ * Declare an aws-materialized effect receipt. Same signature and semantics as
115
+ * core's `EffectReceipt` (#1831) — the options are validated and frozen by the
116
+ * core factory — but the declaration lands in the aws partition, so the aws
117
+ * serializer renders the `AWS::SSM::Parameter` row and the receipt store
118
+ * (../receipt-store.ts) is its writer. Pass the returned const straight to the
119
+ * `effect(...)` op step.
120
+ */
121
+ export function EffectReceipt(name: string, options: EffectReceiptOptions): AwsEffectReceiptDeclaration {
122
+ // Fail at declaration, not at serialize: the effect is the path's final
123
+ // segment, and a name that cannot become a segment has no receipt address.
124
+ if (typeof options?.effect === "string" && options.effect.length > 0) {
125
+ checkSegment("effect", options.effect);
126
+ }
127
+ const core = CoreEffectReceipt(name, options);
128
+ const decl: AwsEffectReceiptDeclaration = {
129
+ [DECLARABLE_MARKER]: true,
130
+ [EFFECT_RECEIPT_MARKER]: true,
131
+ lexicon: "aws",
132
+ entityType: AWS_EFFECT_RECEIPT_ENTITY_TYPE,
133
+ name: core.name,
134
+ effect: core.effect,
135
+ flavor: core.flavor,
136
+ // The same frozen structure the core factory built — intrinsic inputs stay
137
+ // live so discovery can stamp logical names onto attr-refs.
138
+ inputs: core.inputs,
139
+ props: Object.freeze({ Type: "String" as const }),
140
+ };
141
+ // Declared fields immutable, object extensible for discovery's own
142
+ // symbol-keyed metadata — the same lock the core factory applies.
143
+ for (const key of Object.keys(decl)) {
144
+ Object.defineProperty(decl, key, { writable: false, configurable: false });
145
+ }
146
+ return decl;
147
+ }
package/src/index.ts CHANGED
@@ -7,6 +7,21 @@ export type { DefaultTags, TagEntry } from "./default-tags";
7
7
  export { templateTransform, isTemplateTransform, TEMPLATE_TRANSFORM_MARKER } from "./template-transform";
8
8
  export type { TemplateTransform } from "./template-transform";
9
9
 
10
+ // Effect receipts (#1835, epic #1703): the aws materialization row
11
+ // (`AWS::SSM::Parameter`, plain String, path derived from the ownership
12
+ // marker) and the SSM-backed receipt store behind the `effect()` op step.
13
+ export {
14
+ EffectReceipt,
15
+ receiptParameterName,
16
+ AWS_EFFECT_RECEIPT_ENTITY_TYPE,
17
+ EFFECT_RECEIPTS_METADATA_KEY,
18
+ RECEIPT_PATH_PREFIX,
19
+ RECEIPT_UNRESOLVED_VALUE_NOTE,
20
+ } from "./effect-receipt-row";
21
+ export type { AwsEffectReceiptDeclaration } from "./effect-receipt-row";
22
+ export { awsReceiptStore, observeReceiptRows, ssmGetParameter, ssmPutParameter } from "./receipt-store";
23
+ export type { AwsReceiptStoreOptions, ReceiptRowObservation } from "./receipt-store";
24
+
10
25
  // Serializer
11
26
  export { awsSerializer } from "./serializer";
12
27
 
@@ -122,25 +137,31 @@ export {
122
137
  LambdaFunction, LambdaNode, LambdaPython, NodeLambda, PythonLambda,
123
138
  LambdaApi,
124
139
  LambdaScheduled, ScheduledLambda,
125
- LambdaSqs, LambdaEventBridge, LambdaDynamoDB, LambdaS3, LambdaSns,
126
- VpcDefault, FargateAlb, AlbShared, FargateService, RdsInstance, RdsPostgres,
140
+ LambdaSqs, LambdaEventBridge, LambdaDynamoDB, DynamoDBTable, EcrRepository, LambdaS3, LambdaSns,
141
+ VpcDefault, FargateAlb, AlbShared, FargateService, FARGATE_SERVICE_LIMITS, NlbService, RdsInstance, RdsPostgres,
127
142
  EfsWithAccessPoint,
128
- Ec2InstanceRole, MinimalVpc, EksCluster,
143
+ Ec2InstanceRole, Ec2InstanceBundle, MinimalVpc, EksCluster,
129
144
  SolrFargateService,
130
145
  MicrovmApp, MICROVM_LIMITS,
131
- AgentCoreAgent, agentCoreDefaultEndpointArn,
146
+ AgentCoreAgent, agentCoreDefaultEndpointArn, AGENTCORE_LIMITS,
132
147
  OrganizationRoot, GovernanceFoundation, RegionRestriction, OrganizationTrail,
148
+ StepFunctionsWorkflow,
149
+ MonitoringStack,
150
+ BucketDeployment,
133
151
  } from "./composites/index";
134
152
  export type {
135
153
  LambdaFunctionProps, LambdaApiProps, ScheduledLambdaProps,
136
- LambdaSqsProps, LambdaEventBridgeProps, LambdaDynamoDBProps, LambdaS3Props, LambdaSnsProps,
137
- VpcDefaultProps, FargateAlbProps, AlbSharedProps, FargateServiceProps, RdsInstanceProps, RdsPostgresProps,
154
+ LambdaSqsProps, LambdaEventBridgeProps, LambdaDynamoDBProps, DynamoDBTableProps, DynamoDBKey, DynamoDBCapacity, DynamoDBGlobalSecondaryIndex, EcrRepositoryProps, EcrLifecycleRule, EcrEncryption, LambdaS3Props, LambdaSnsProps,
155
+ VpcDefaultProps, FargateAlbProps, AlbSharedProps, FargateServiceProps, NlbServiceProps, RdsInstanceProps, RdsPostgresProps,
138
156
  EfsWithAccessPointProps,
139
- Ec2InstanceRoleProps, MinimalVpcProps, EksClusterProps,
157
+ Ec2InstanceRoleProps, Ec2InstanceBundleProps, Ec2InstanceBundleIngressRule, MinimalVpcProps, EksClusterProps,
140
158
  SolrFargateServiceProps,
141
159
  MicrovmAppProps, MicrovmAppResult, MicrovmAppBuildConnectorProps, MicrovmMemoryMiB,
142
160
  AgentCoreAgentProps, AgentCoreAgentResult,
143
161
  OrganizationRootProps, GovernanceFoundationProps, RegionRestrictionProps, OrganizationTrailProps,
162
+ StepFunctionsWorkflowProps,
163
+ MonitoringStackProps, MonitoringMetricSpec,
164
+ BucketDeploymentProps, BucketDeploymentResult,
144
165
  } from "./composites/index";
145
166
 
146
167
  // Code generation pipeline
@@ -72,4 +72,10 @@ export const awsAuditCatalog: Record<string, RuleMeta> = {
72
72
  // so no authority citation (those are reserved for merge-worthy entries).
73
73
  WAW059: auditRule("WAW059", "report-only", "guidance", "Wildcard Resource where the declared graph enumerates the touched set", "Tighten Resource from \"*\" to the Fn::GetAtt Arn list of the declared resources the role's consumers touch.", { category: "security" }),
74
74
  WAW060: auditRule("WAW060", "report-only", "guidance", "IAM policy attached to no principal", "Attach the policy via Roles/Users/Groups or reference it from a principal's ManagedPolicyArns — unattached it grants nothing.", { category: "security" }),
75
+
76
+ // #1140 — error-injection benchmark family (companion to epic #1139): static
77
+ // catches for the deploy-time/runtime failure class stock CDK can't see pre-synth.
78
+ WAW061: auditRule("WAW061", "merge-worthy", "guidance", "Subnet CidrBlock not contained in its VPC's CidrBlock", "Fix the subnet's CidrBlock so it falls within the VPC's CidrBlock range.", { category: "correctness" }),
79
+ WAW062: auditRule("WAW062", "merge-worthy", "guidance", "Duplicate export name or explicit resource name within a template", "Give each duplicated Export/name a distinct literal value.", { category: "correctness" }),
80
+ WAW063: auditRule("WAW063", "merge-worthy", "guidance", "IAM policy denies an action another attached policy on the same role allows", "Remove or narrow the Deny, or move the Allow off this role — the explicit Deny always wins.", { category: "correctness" }),
75
81
  };
@@ -169,6 +169,46 @@ export function isFullTierEnv(env: string | undefined): boolean {
169
169
  return env === "prod" || env === "production" || env === "full";
170
170
  }
171
171
 
172
+ /**
173
+ * Parse an IPv4 CIDR literal ("10.0.0.0/16") into its numeric base address
174
+ * and prefix length. Returns null for anything that isn't a plain IPv4 CIDR
175
+ * (IPv6, malformed octets, out-of-range prefix) — callers treat that as
176
+ * "can't prove statically" rather than an error.
177
+ */
178
+ export function parseIpv4Cidr(cidr: string): { base: number; prefix: number } | null {
179
+ const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})\/(\d{1,2})$/.exec(cidr.trim());
180
+ if (!m) return null;
181
+ const octets = m.slice(1, 5).map(Number);
182
+ if (octets.some((o) => o < 0 || o > 255)) return null;
183
+ const prefix = Number(m[5]);
184
+ if (prefix < 0 || prefix > 32) return null;
185
+ const base = ((octets[0] << 24) | (octets[1] << 16) | (octets[2] << 8) | octets[3]) >>> 0;
186
+ return { base, prefix };
187
+ }
188
+
189
+ /** [networkAddress, broadcastAddress] for a parsed IPv4 CIDR block. */
190
+ function ipv4Range(cidr: { base: number; prefix: number }): [number, number] {
191
+ const maskBits = cidr.prefix === 0 ? 0 : (0xffffffff << (32 - cidr.prefix)) >>> 0;
192
+ const network = (cidr.base & maskBits) >>> 0;
193
+ const broadcast = (network | (~maskBits >>> 0)) >>> 0;
194
+ return [network, broadcast];
195
+ }
196
+
197
+ /**
198
+ * Whether `inner` (an IPv4 CIDR literal) falls entirely within `outer`.
199
+ * Returns null — not false — when either literal isn't a plain, parseable
200
+ * IPv4 CIDR (e.g. IPv6, or a CloudFormation intrinsic already stringified
201
+ * elsewhere); a null means "statically unprovable", not "violation".
202
+ */
203
+ export function ipv4CidrContains(outer: string, inner: string): boolean | null {
204
+ const outerCidr = parseIpv4Cidr(outer);
205
+ const innerCidr = parseIpv4Cidr(inner);
206
+ if (!outerCidr || !innerCidr) return null;
207
+ const [oStart, oEnd] = ipv4Range(outerCidr);
208
+ const [iStart, iEnd] = ipv4Range(innerCidr);
209
+ return iStart >= oStart && iEnd <= oEnd;
210
+ }
211
+
172
212
  /**
173
213
  * Check if a port range [fromPort, toPort] contains any of the sensitive ports.
174
214
  */
@@ -52,6 +52,9 @@ import { waw057 } from "./waw057";
52
52
  import { waw058 } from "./waw058";
53
53
  import { waw059 } from "./waw059";
54
54
  import { waw060 } from "./waw060";
55
+ import { waw061 } from "./waw061";
56
+ import { waw062 } from "./waw062";
57
+ import { waw063 } from "./waw063";
55
58
 
56
59
  export const postSynthChecks: PostSynthCheck[] = [
57
60
  cor020,
@@ -106,4 +109,7 @@ export const postSynthChecks: PostSynthCheck[] = [
106
109
  waw058,
107
110
  waw059,
108
111
  waw060,
112
+ waw061,
113
+ waw062,
114
+ waw063,
109
115
  ];
@@ -0,0 +1,99 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import { createPostSynthContext } from "@intentius/chant-test-utils";
3
+ import { waw061, checkSubnetCidrContainment } from "./waw061";
4
+
5
+ function makeCtx(template: object) {
6
+ return createPostSynthContext({ aws: template });
7
+ }
8
+
9
+ describe("WAW061: Subnet CIDR Not Contained In VPC CIDR", () => {
10
+ test("check metadata", () => {
11
+ expect(waw061.id).toBe("WAW061");
12
+ expect(waw061.description).toContain("CidrBlock");
13
+ });
14
+
15
+ test("subnet CIDR outside the VPC's CIDR range → error", () => {
16
+ const ctx = makeCtx({
17
+ Resources: {
18
+ MyVpc: { Type: "AWS::EC2::VPC", Properties: { CidrBlock: "10.0.0.0/16" } },
19
+ MySubnet: {
20
+ Type: "AWS::EC2::Subnet",
21
+ Properties: { CidrBlock: "10.1.0.0/24", VpcId: { Ref: "MyVpc" } },
22
+ },
23
+ },
24
+ });
25
+ const diags = checkSubnetCidrContainment(ctx);
26
+ expect(diags).toHaveLength(1);
27
+ expect(diags[0].checkId).toBe("WAW061");
28
+ expect(diags[0].severity).toBe("error");
29
+ expect(diags[0].entity).toBe("MySubnet");
30
+ expect(diags[0].message).toContain("10.1.0.0/24");
31
+ expect(diags[0].message).toContain("10.0.0.0/16");
32
+ expect(diags[0].lexicon).toBe("aws");
33
+ });
34
+
35
+ test("subnet wider than its VPC → error", () => {
36
+ const ctx = makeCtx({
37
+ Resources: {
38
+ MyVpc: { Type: "AWS::EC2::VPC", Properties: { CidrBlock: "10.0.1.0/24" } },
39
+ MySubnet: {
40
+ Type: "AWS::EC2::Subnet",
41
+ Properties: { CidrBlock: "10.0.0.0/16", VpcId: { Ref: "MyVpc" } },
42
+ },
43
+ },
44
+ });
45
+ expect(checkSubnetCidrContainment(ctx)).toHaveLength(1);
46
+ });
47
+
48
+ test("subnet CIDR contained in the VPC's CIDR → no diagnostic", () => {
49
+ const ctx = makeCtx({
50
+ Resources: {
51
+ MyVpc: { Type: "AWS::EC2::VPC", Properties: { CidrBlock: "10.0.0.0/16" } },
52
+ MySubnet: {
53
+ Type: "AWS::EC2::Subnet",
54
+ Properties: { CidrBlock: "10.0.1.0/24", VpcId: { Ref: "MyVpc" } },
55
+ },
56
+ },
57
+ });
58
+ expect(checkSubnetCidrContainment(ctx)).toHaveLength(0);
59
+ });
60
+
61
+ test("intrinsic CidrBlock → no diagnostic (unprovable)", () => {
62
+ const ctx = makeCtx({
63
+ Resources: {
64
+ MyVpc: { Type: "AWS::EC2::VPC", Properties: { CidrBlock: "10.0.0.0/16" } },
65
+ MySubnet: {
66
+ Type: "AWS::EC2::Subnet",
67
+ Properties: {
68
+ CidrBlock: { "Fn::Select": [0, { "Fn::Cidr": [{ "Fn::GetAtt": ["MyVpc", "CidrBlock"] }, 4, 8] }] },
69
+ VpcId: { Ref: "MyVpc" },
70
+ },
71
+ },
72
+ },
73
+ });
74
+ expect(checkSubnetCidrContainment(ctx)).toHaveLength(0);
75
+ });
76
+
77
+ test("VpcId not resolvable to a declared VPC (imported) → no diagnostic", () => {
78
+ const ctx = makeCtx({
79
+ Resources: {
80
+ MySubnet: {
81
+ Type: "AWS::EC2::Subnet",
82
+ Properties: { CidrBlock: "10.1.0.0/24", VpcId: { "Fn::ImportValue": "shared-vpc-id" } },
83
+ },
84
+ },
85
+ });
86
+ expect(checkSubnetCidrContainment(ctx)).toHaveLength(0);
87
+ });
88
+
89
+ test("multiple subnets in a VPC, all contained → no diagnostic", () => {
90
+ const ctx = makeCtx({
91
+ Resources: {
92
+ MyVpc: { Type: "AWS::EC2::VPC", Properties: { CidrBlock: "10.0.0.0/16" } },
93
+ SubnetA: { Type: "AWS::EC2::Subnet", Properties: { CidrBlock: "10.0.0.0/24", VpcId: { Ref: "MyVpc" } } },
94
+ SubnetB: { Type: "AWS::EC2::Subnet", Properties: { CidrBlock: "10.0.1.0/24", VpcId: { Ref: "MyVpc" } } },
95
+ },
96
+ });
97
+ expect(checkSubnetCidrContainment(ctx)).toHaveLength(0);
98
+ });
99
+ });
@@ -0,0 +1,67 @@
1
+ /**
2
+ * WAW061: Subnet CIDR Not Contained In VPC CIDR
3
+ *
4
+ * A subnet's CidrBlock must fall entirely inside its VPC's CidrBlock.
5
+ * Stock CDK does not check this — CloudFormation accepts the template and
6
+ * only fails the `AWS::EC2::Subnet` resource at deploy time
7
+ * ("The CIDR ... is invalid" / "does not fall within the CIDR range of the
8
+ * VPC"), after the rest of the stack may have already started rolling out.
9
+ * This is a cheap static graph check: follow the subnet's VpcId Ref to a
10
+ * declared VPC in the same template and do IPv4 CIDR-range math.
11
+ *
12
+ * Stays quiet whenever the graph can't prove it: intrinsic CIDR blocks,
13
+ * IPv6, a VpcId that isn't a simple Ref to a declared AWS::EC2::VPC, or a
14
+ * VPC whose own CidrBlock isn't a literal string.
15
+ */
16
+
17
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
18
+ import { parseCFTemplate, findResourceRefs, ipv4CidrContains } from "./cf-refs";
19
+
20
+ export function checkSubnetCidrContainment(ctx: PostSynthContext): PostSynthDiagnostic[] {
21
+ const diagnostics: PostSynthDiagnostic[] = [];
22
+
23
+ for (const [_lexicon, output] of ctx.outputs) {
24
+ const template = parseCFTemplate(output);
25
+ if (!template?.Resources) continue;
26
+
27
+ for (const [logicalId, resource] of Object.entries(template.Resources)) {
28
+ if (resource.Type !== "AWS::EC2::Subnet") continue;
29
+
30
+ const props = resource.Properties ?? {};
31
+ const subnetCidr = props.CidrBlock;
32
+ if (typeof subnetCidr !== "string") continue;
33
+
34
+ const vpcRefs = findResourceRefs(props.VpcId);
35
+ if (vpcRefs.size !== 1) continue;
36
+ const [vpcId] = vpcRefs;
37
+
38
+ const vpc = template.Resources[vpcId];
39
+ if (!vpc || vpc.Type !== "AWS::EC2::VPC") continue;
40
+
41
+ const vpcCidr = vpc.Properties?.CidrBlock;
42
+ if (typeof vpcCidr !== "string") continue;
43
+
44
+ const contained = ipv4CidrContains(vpcCidr, subnetCidr);
45
+ if (contained === false) {
46
+ diagnostics.push({
47
+ checkId: "WAW061",
48
+ severity: "error",
49
+ message: `Subnet "${logicalId}" CidrBlock ${subnetCidr} is not contained within VPC "${vpcId}"'s CidrBlock ${vpcCidr} — CloudFormation rejects this at deploy time`,
50
+ entity: logicalId,
51
+ lexicon: "aws",
52
+ });
53
+ }
54
+ }
55
+ }
56
+
57
+ return diagnostics;
58
+ }
59
+
60
+ export const waw061: PostSynthCheck = {
61
+ id: "WAW061",
62
+ description: "Subnet CidrBlock falls outside its VPC's CidrBlock — fails at deploy time",
63
+
64
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
65
+ return checkSubnetCidrContainment(ctx);
66
+ },
67
+ };