@intentius/chant-lexicon-aws 0.45.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.
- package/dist/api/read-client.d.ts +10 -2
- package/dist/api/read-client.d.ts.map +1 -1
- package/dist/components/cloud-executor.d.ts.map +1 -1
- package/dist/composites/agentcore-agent.d.ts +26 -1
- package/dist/composites/agentcore-agent.d.ts.map +1 -1
- package/dist/composites/bucket-deployment.d.ts +82 -0
- package/dist/composites/bucket-deployment.d.ts.map +1 -0
- package/dist/composites/dynamodb-table.d.ts +45 -0
- package/dist/composites/dynamodb-table.d.ts.map +1 -0
- package/dist/composites/ec2-instance-bundle.d.ts +61 -0
- package/dist/composites/ec2-instance-bundle.d.ts.map +1 -0
- package/dist/composites/ecr-repository.d.ts +49 -0
- package/dist/composites/ecr-repository.d.ts.map +1 -0
- package/dist/composites/fargate-service.d.ts +16 -0
- package/dist/composites/fargate-service.d.ts.map +1 -1
- package/dist/composites/index.d.ts +16 -2
- package/dist/composites/index.d.ts.map +1 -1
- package/dist/composites/lambda-function.d.ts +4 -4
- package/dist/composites/lambda-function.d.ts.map +1 -1
- package/dist/composites/monitoring-stack.d.ts +99 -0
- package/dist/composites/monitoring-stack.d.ts.map +1 -0
- package/dist/composites/nlb-service.d.ts +48 -0
- package/dist/composites/nlb-service.d.ts.map +1 -0
- package/dist/composites/step-functions-workflow.d.ts +36 -0
- package/dist/composites/step-functions-workflow.d.ts.map +1 -0
- package/dist/deep-observe.d.ts +45 -0
- package/dist/deep-observe.d.ts.map +1 -1
- package/dist/deep-topology.d.ts +104 -0
- package/dist/deep-topology.d.ts.map +1 -0
- package/dist/effect-receipt-row.d.ts +92 -0
- package/dist/effect-receipt-row.d.ts.map +1 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +9 -4
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/lint/post-synth/cf-refs.d.ts +24 -0
- package/dist/lint/post-synth/cf-refs.d.ts.map +1 -1
- package/dist/lint/post-synth/index.d.ts.map +1 -1
- package/dist/lint/post-synth/waw059.d.ts +36 -0
- package/dist/lint/post-synth/waw059.d.ts.map +1 -0
- package/dist/lint/post-synth/waw060.d.ts +16 -0
- package/dist/lint/post-synth/waw060.d.ts.map +1 -0
- package/dist/lint/post-synth/waw061.d.ts +19 -0
- package/dist/lint/post-synth/waw061.d.ts.map +1 -0
- package/dist/lint/post-synth/waw062.d.ts +19 -0
- package/dist/lint/post-synth/waw062.d.ts.map +1 -0
- package/dist/lint/post-synth/waw063.d.ts +35 -0
- package/dist/lint/post-synth/waw063.d.ts.map +1 -0
- package/dist/manifest.json +1 -1
- package/dist/okf/index.md +5 -0
- package/dist/okf/rules/WAW059.md +25 -0
- package/dist/okf/rules/WAW060.md +17 -0
- package/dist/okf/rules/WAW061.md +17 -0
- package/dist/okf/rules/WAW062.md +27 -0
- package/dist/okf/rules/WAW063.md +19 -0
- package/dist/okf/types/Action.md +2 -0
- package/dist/okf/types/AutoScalingGroup.md +1 -0
- package/dist/okf/types/Bucket.md +2 -0
- package/dist/okf/types/ECRRepository.md +1 -0
- package/dist/okf/types/Function.md +1 -0
- package/dist/okf/types/GlobalTable.md +4 -0
- package/dist/okf/types/IamPolicy.md +3 -0
- package/dist/okf/types/InstanceProfile.md +4 -0
- package/dist/okf/types/LaunchTemplate.md +1 -0
- package/dist/okf/types/LoadBalancer.md +1 -0
- package/dist/okf/types/ManagedPolicy.md +4 -0
- package/dist/okf/types/Map.md +2 -0
- package/dist/okf/types/Queue.md +1 -0
- package/dist/okf/types/Role.md +3 -0
- package/dist/okf/types/SecurityGroup.md +1 -0
- package/dist/okf/types/StateMachine.md +4 -0
- package/dist/okf/types/Subnet.md +4 -0
- package/dist/okf/types/Table.md +2 -0
- package/dist/okf/types/Type.md +5 -0
- package/dist/okf/types/Vpc.md +1 -0
- package/dist/op/activities/aws-apply.d.ts +48 -5
- package/dist/op/activities/aws-apply.d.ts.map +1 -1
- package/dist/op/activities/index.d.ts +11 -4
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/ownership.d.ts +18 -0
- package/dist/ownership.d.ts.map +1 -1
- package/dist/plugin.d.ts.map +1 -1
- package/dist/receipt-store.d.ts +109 -0
- package/dist/receipt-store.d.ts.map +1 -0
- package/dist/rules/cf-refs.ts +62 -0
- package/dist/rules/waw059.ts +353 -0
- package/dist/rules/waw060.ts +91 -0
- package/dist/rules/waw061.ts +67 -0
- package/dist/rules/waw062.ts +0 -0
- package/dist/rules/waw063.ts +0 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/skills/chant-aws-carve-terraform.md +26 -23
- package/dist/teardown.d.ts +85 -0
- package/dist/teardown.d.ts.map +1 -0
- package/package.json +3 -3
- package/src/api/read-client.test.ts +9 -0
- package/src/api/read-client.ts +15 -2
- package/src/components/cloud-executor.ts +10 -1
- package/src/composites/agentcore-agent.ts +35 -13
- package/src/composites/bucket-deployment.test.ts +84 -0
- package/src/composites/bucket-deployment.ts +153 -0
- package/src/composites/composites.test.ts +325 -0
- package/src/composites/dynamodb-table.test.ts +154 -0
- package/src/composites/dynamodb-table.ts +124 -0
- package/src/composites/ec2-instance-bundle.ts +134 -0
- package/src/composites/ecr-repository.test.ts +118 -0
- package/src/composites/ecr-repository.ts +117 -0
- package/src/composites/fargate-service.ts +19 -3
- package/src/composites/index.ts +21 -2
- package/src/composites/monitoring-stack.test.ts +130 -0
- package/src/composites/monitoring-stack.ts +164 -0
- package/src/composites/nlb-service.ts +117 -0
- package/src/composites/step-functions-workflow.ts +127 -0
- package/src/deep-observe.test.ts +201 -1
- package/src/deep-observe.ts +204 -0
- package/src/deep-topology.test.ts +392 -0
- package/src/deep-topology.ts +237 -0
- package/src/effect-receipt-row.test.ts +164 -0
- package/src/effect-receipt-row.ts +147 -0
- package/src/index.ts +28 -7
- package/src/lifecycle-integration.test.ts +4 -0
- package/src/lint/audit-catalog.ts +11 -0
- package/src/lint/post-synth/cf-refs.ts +62 -0
- package/src/lint/post-synth/index.ts +10 -0
- package/src/lint/post-synth/waw059.test.ts +309 -0
- package/src/lint/post-synth/waw059.ts +353 -0
- package/src/lint/post-synth/waw060.test.ts +131 -0
- package/src/lint/post-synth/waw060.ts +91 -0
- package/src/lint/post-synth/waw061.test.ts +99 -0
- package/src/lint/post-synth/waw061.ts +67 -0
- package/src/lint/post-synth/waw062.test.ts +109 -0
- package/src/lint/post-synth/waw062.ts +0 -0
- package/src/lint/post-synth/waw063.test.ts +220 -0
- package/src/lint/post-synth/waw063.ts +0 -0
- package/src/op/activities/aws-apply.test.ts +111 -0
- package/src/op/activities/aws-apply.ts +100 -6
- package/src/op/activities/index.ts +20 -3
- package/src/ownership.test.ts +24 -1
- package/src/ownership.ts +37 -0
- package/src/plugin.ts +59 -10
- package/src/receipt-store.test.ts +204 -0
- package/src/receipt-store.ts +315 -0
- package/src/serializer-ownership.test.ts +18 -0
- package/src/serializer.ts +114 -3
- package/src/skills/chant-aws-carve-terraform.md +26 -23
- package/src/teardown.test.ts +258 -0
- package/src/teardown.ts +276 -0
|
@@ -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
|
|
@@ -428,9 +428,13 @@ describeObservationConformance({
|
|
|
428
428
|
},
|
|
429
429
|
},
|
|
430
430
|
{
|
|
431
|
+
// describe-stack-resources also carries no tags to read a stack/env
|
|
432
|
+
// identity from, so `marker` (#1222) stays absent here — stack-level
|
|
433
|
+
// handling reads the stack's own tags instead.
|
|
431
434
|
name: "a healthy stack read",
|
|
432
435
|
declared: ["MyBucket"],
|
|
433
436
|
expectPresent: ["MyBucket"],
|
|
437
|
+
expectNoMarker: ["MyBucket"],
|
|
434
438
|
run: () => {
|
|
435
439
|
stubCfn((action) =>
|
|
436
440
|
action === "DescribeStackResources"
|
|
@@ -67,4 +67,15 @@ export const awsAuditCatalog: Record<string, RuleMeta> = {
|
|
|
67
67
|
WAW056: auditRule("WAW056", "merge-worthy", "guidance", "SCP guardrail has no Deny statement", "Add a Deny statement — SCPs only filter permissions, so a Deny-less SCP guards nothing.", { authority: [AWS_SEC] }),
|
|
68
68
|
WAW057: auditRule("WAW057", "merge-worthy", "guidance", "SCP guardrail attached to no targets", "Attach the SCP to the organization root or an OU via TargetIds.", { authority: [AWS_SEC] }),
|
|
69
69
|
WAW058: auditRule("WAW058", "merge-worthy", "guidance", "Organization audit trail dropped or scoped down", "Keep an organization CloudTrail with IsLogging: true and IsMultiRegionTrail: true.", { authority: [AWS_SEC] }),
|
|
70
|
+
|
|
71
|
+
// #1225 — least-privilege tightening the declared graph can prove. Report-only,
|
|
72
|
+
// so no authority citation (those are reserved for merge-worthy entries).
|
|
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
|
+
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" }),
|
|
70
81
|
};
|
|
@@ -50,6 +50,28 @@ export function findResourceRefs(value: unknown): Set<string> {
|
|
|
50
50
|
return refs;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
/**
|
|
54
|
+
* Build the reverse of `findResourceRefs` for a whole template: logical id →
|
|
55
|
+
* the set of logical ids whose Properties reference it via Ref/Fn::GetAtt.
|
|
56
|
+
* Lets a check walk the graph consumer-ward (who uses this role?) instead of
|
|
57
|
+
* dependency-ward. Used by WAW059.
|
|
58
|
+
*/
|
|
59
|
+
export function buildReverseRefIndex(template: CFTemplate): Map<string, Set<string>> {
|
|
60
|
+
const index = new Map<string, Set<string>>();
|
|
61
|
+
for (const [logicalId, resource] of Object.entries(template.Resources ?? {})) {
|
|
62
|
+
for (const target of findResourceRefs(resource.Properties)) {
|
|
63
|
+
if (target === logicalId) continue;
|
|
64
|
+
let consumers = index.get(target);
|
|
65
|
+
if (!consumers) {
|
|
66
|
+
consumers = new Set();
|
|
67
|
+
index.set(target, consumers);
|
|
68
|
+
}
|
|
69
|
+
consumers.add(logicalId);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return index;
|
|
73
|
+
}
|
|
74
|
+
|
|
53
75
|
/**
|
|
54
76
|
* Check if a value is a CloudFormation intrinsic function (Ref, Fn::*, etc.)
|
|
55
77
|
* that cannot be statically evaluated.
|
|
@@ -147,6 +169,46 @@ export function isFullTierEnv(env: string | undefined): boolean {
|
|
|
147
169
|
return env === "prod" || env === "production" || env === "full";
|
|
148
170
|
}
|
|
149
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
|
+
|
|
150
212
|
/**
|
|
151
213
|
* Check if a port range [fromPort, toPort] contains any of the sensitive ports.
|
|
152
214
|
*/
|
|
@@ -50,6 +50,11 @@ import { waw055 } from "./waw055";
|
|
|
50
50
|
import { waw056 } from "./waw056";
|
|
51
51
|
import { waw057 } from "./waw057";
|
|
52
52
|
import { waw058 } from "./waw058";
|
|
53
|
+
import { waw059 } from "./waw059";
|
|
54
|
+
import { waw060 } from "./waw060";
|
|
55
|
+
import { waw061 } from "./waw061";
|
|
56
|
+
import { waw062 } from "./waw062";
|
|
57
|
+
import { waw063 } from "./waw063";
|
|
53
58
|
|
|
54
59
|
export const postSynthChecks: PostSynthCheck[] = [
|
|
55
60
|
cor020,
|
|
@@ -102,4 +107,9 @@ export const postSynthChecks: PostSynthCheck[] = [
|
|
|
102
107
|
waw056,
|
|
103
108
|
waw057,
|
|
104
109
|
waw058,
|
|
110
|
+
waw059,
|
|
111
|
+
waw060,
|
|
112
|
+
waw061,
|
|
113
|
+
waw062,
|
|
114
|
+
waw063,
|
|
105
115
|
];
|