@intentius/chant-lexicon-cedar 0.44.8 → 0.44.10

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 (149) hide show
  1. package/README.md +106 -2
  2. package/dist/agentcore/embed.d.ts +189 -0
  3. package/dist/agentcore/embed.d.ts.map +1 -0
  4. package/dist/agentcore/enforcement.d.ts +76 -0
  5. package/dist/agentcore/enforcement.d.ts.map +1 -0
  6. package/dist/agentcore/scan.d.ts +46 -0
  7. package/dist/agentcore/scan.d.ts.map +1 -0
  8. package/dist/codegen/docs-dogwood.d.ts +21 -0
  9. package/dist/codegen/docs-dogwood.d.ts.map +1 -0
  10. package/dist/codegen/docs.d.ts.map +1 -1
  11. package/dist/codegen/package.d.ts.map +1 -1
  12. package/dist/config.d.ts +25 -0
  13. package/dist/config.d.ts.map +1 -1
  14. package/dist/dogwood/cli.d.ts +247 -0
  15. package/dist/dogwood/cli.d.ts.map +1 -0
  16. package/dist/dogwood/event-schema.d.ts +161 -0
  17. package/dist/dogwood/event-schema.d.ts.map +1 -0
  18. package/dist/dogwood/index.d.ts +39 -0
  19. package/dist/dogwood/index.d.ts.map +1 -0
  20. package/dist/dogwood/macros.d.ts +96 -0
  21. package/dist/dogwood/macros.d.ts.map +1 -0
  22. package/dist/dogwood/policy.d.ts +120 -0
  23. package/dist/dogwood/policy.d.ts.map +1 -0
  24. package/dist/dogwood/replay-activity.d.ts +196 -0
  25. package/dist/dogwood/replay-activity.d.ts.map +1 -0
  26. package/dist/dogwood/replay-op.d.ts +165 -0
  27. package/dist/dogwood/replay-op.d.ts.map +1 -0
  28. package/dist/dogwood/scan.d.ts +109 -0
  29. package/dist/dogwood/scan.d.ts.map +1 -0
  30. package/dist/dogwood/serialize.d.ts +66 -0
  31. package/dist/dogwood/serialize.d.ts.map +1 -0
  32. package/dist/dogwood/temporal.d.ts +259 -0
  33. package/dist/dogwood/temporal.d.ts.map +1 -0
  34. package/dist/dogwood/trace.d.ts +215 -0
  35. package/dist/dogwood/trace.d.ts.map +1 -0
  36. package/dist/dogwood/upstream.d.ts +41 -0
  37. package/dist/dogwood/upstream.d.ts.map +1 -0
  38. package/dist/dogwood/window.d.ts +73 -0
  39. package/dist/dogwood/window.d.ts.map +1 -0
  40. package/dist/index.d.ts +12 -0
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/integrity.json +15 -3
  43. package/dist/lint/audit-catalog.d.ts.map +1 -1
  44. package/dist/lint/post-synth/dogwood-helpers.d.ts +63 -0
  45. package/dist/lint/post-synth/dogwood-helpers.d.ts.map +1 -0
  46. package/dist/lint/post-synth/dwdc010.d.ts +25 -0
  47. package/dist/lint/post-synth/dwdc010.d.ts.map +1 -0
  48. package/dist/lint/post-synth/dwdc011.d.ts +19 -0
  49. package/dist/lint/post-synth/dwdc011.d.ts.map +1 -0
  50. package/dist/lint/post-synth/dwdc012.d.ts +21 -0
  51. package/dist/lint/post-synth/dwdc012.d.ts.map +1 -0
  52. package/dist/lint/post-synth/dwdc013.d.ts +33 -0
  53. package/dist/lint/post-synth/dwdc013.d.ts.map +1 -0
  54. package/dist/lint/post-synth/dwde010.d.ts +32 -0
  55. package/dist/lint/post-synth/dwde010.d.ts.map +1 -0
  56. package/dist/lint/post-synth/dwde011.d.ts +33 -0
  57. package/dist/lint/post-synth/dwde011.d.ts.map +1 -0
  58. package/dist/lint/post-synth/dwds010.d.ts +24 -0
  59. package/dist/lint/post-synth/dwds010.d.ts.map +1 -0
  60. package/dist/lint/post-synth/index.d.ts.map +1 -1
  61. package/dist/manifest.json +1 -1
  62. package/dist/okf/index.md +7 -0
  63. package/dist/okf/rules/DWDC010.md +11 -0
  64. package/dist/okf/rules/DWDC011.md +11 -0
  65. package/dist/okf/rules/DWDC012.md +11 -0
  66. package/dist/okf/rules/DWDC013.md +15 -0
  67. package/dist/okf/rules/DWDE010.md +11 -0
  68. package/dist/okf/rules/DWDE011.md +11 -0
  69. package/dist/okf/rules/DWDS010.md +11 -0
  70. package/dist/okf/types/Policy.md +1 -0
  71. package/dist/op/activities/index.d.ts +18 -0
  72. package/dist/op/activities/index.d.ts.map +1 -0
  73. package/dist/plugin.d.ts.map +1 -1
  74. package/dist/policy-text.d.ts +53 -0
  75. package/dist/policy-text.d.ts.map +1 -0
  76. package/dist/rules/dogwood-helpers.ts +139 -0
  77. package/dist/rules/dwdc010.ts +62 -0
  78. package/dist/rules/dwdc011.ts +61 -0
  79. package/dist/rules/dwdc012.ts +46 -0
  80. package/dist/rules/dwdc013.ts +64 -0
  81. package/dist/rules/dwde010.ts +130 -0
  82. package/dist/rules/dwde011.ts +108 -0
  83. package/dist/rules/dwds010.ts +46 -0
  84. package/dist/serializer.d.ts +10 -18
  85. package/dist/serializer.d.ts.map +1 -1
  86. package/dist/skills/chant-cedar-authoring.md +180 -0
  87. package/dist/skills/chant-cedar-avp-embedding.md +125 -0
  88. package/dist/skills/chant-cedar-dogwood.md +327 -0
  89. package/dist/skills/chant-cedar-meta-policy.md +119 -0
  90. package/package.json +7 -2
  91. package/src/agentcore/embed.test.ts +254 -0
  92. package/src/agentcore/embed.ts +399 -0
  93. package/src/agentcore/enforcement.test.ts +43 -0
  94. package/src/agentcore/enforcement.ts +92 -0
  95. package/src/agentcore/scan.ts +119 -0
  96. package/src/codegen/docs-dogwood.ts +1119 -0
  97. package/src/codegen/docs.ts +66 -1
  98. package/src/codegen/package.ts +3 -2
  99. package/src/config.test.ts +12 -0
  100. package/src/config.ts +28 -0
  101. package/src/dogwood/cli.test.ts +513 -0
  102. package/src/dogwood/cli.ts +666 -0
  103. package/src/dogwood/event-schema.test.ts +218 -0
  104. package/src/dogwood/event-schema.ts +318 -0
  105. package/src/dogwood/index.ts +271 -0
  106. package/src/dogwood/macros.test.ts +104 -0
  107. package/src/dogwood/macros.ts +229 -0
  108. package/src/dogwood/policy.test.ts +94 -0
  109. package/src/dogwood/policy.ts +141 -0
  110. package/src/dogwood/replay-activity.test.ts +481 -0
  111. package/src/dogwood/replay-activity.ts +506 -0
  112. package/src/dogwood/replay-op.ts +242 -0
  113. package/src/dogwood/scan.ts +287 -0
  114. package/src/dogwood/serialize.test.ts +331 -0
  115. package/src/dogwood/serialize.ts +246 -0
  116. package/src/dogwood/temporal.test.ts +272 -0
  117. package/src/dogwood/temporal.ts +592 -0
  118. package/src/dogwood/testdata/custom-kinds.dwschema +17 -0
  119. package/src/dogwood/testdata/default-macros.dw +23 -0
  120. package/src/dogwood/testdata/lowered-read-after-login.json +13 -0
  121. package/src/dogwood/testdata/max-window-raised.dwschema +25 -0
  122. package/src/dogwood/testdata/pinned.dwschema +31 -0
  123. package/src/dogwood/testdata/read-after-login.cedarschema +20 -0
  124. package/src/dogwood/testdata/read-after-login.dw +17 -0
  125. package/src/dogwood/testdata/temporal-policies.dw +53 -0
  126. package/src/dogwood/trace.test.ts +231 -0
  127. package/src/dogwood/trace.ts +471 -0
  128. package/src/dogwood/upstream.ts +41 -0
  129. package/src/dogwood/window.ts +124 -0
  130. package/src/index.ts +76 -0
  131. package/src/lint/audit-catalog.ts +64 -0
  132. package/src/lint/post-synth/dogwood-helpers.ts +139 -0
  133. package/src/lint/post-synth/dwd-post-synth.test.ts +374 -0
  134. package/src/lint/post-synth/dwdc010.ts +62 -0
  135. package/src/lint/post-synth/dwdc011.ts +61 -0
  136. package/src/lint/post-synth/dwdc012.ts +46 -0
  137. package/src/lint/post-synth/dwdc013.ts +64 -0
  138. package/src/lint/post-synth/dwde-post-synth.test.ts +368 -0
  139. package/src/lint/post-synth/dwde010.ts +130 -0
  140. package/src/lint/post-synth/dwde011.ts +108 -0
  141. package/src/lint/post-synth/dwds010.ts +46 -0
  142. package/src/lint/post-synth/index.ts +14 -0
  143. package/src/lint/post-synth/post-synth.test.ts +7 -3
  144. package/src/op/activities/index.ts +27 -0
  145. package/src/plugin.test.ts +3 -2
  146. package/src/plugin.ts +30 -0
  147. package/src/policy-text.ts +128 -0
  148. package/src/serializer.ts +71 -109
  149. package/src/skills/chant-cedar-dogwood.md +327 -0
package/src/index.ts CHANGED
@@ -54,6 +54,82 @@ export type { CedarGenerateOptions, CedarGenerateResult } from "./import/generat
54
54
  export { CedarTemplateParser, CedarTemplateGenerator } from "./import/adapter";
55
55
  export { detectTemplate } from "./detect";
56
56
 
57
+ // AgentCore embedding (#1660) — the typed statement an
58
+ // `AWS::BedrockAgentCore::Policy` carries on either arm of its Definition
59
+ // union, and the EnforcementMode dial that stages the rollout. Same
60
+ // no-dependency rule as the AVP seam above. See src/agentcore/embed.ts.
61
+ export {
62
+ agentCoreStatement,
63
+ agentCorePolicyDefinition,
64
+ agentCorePolicyResource,
65
+ agentCorePolicySet,
66
+ agentCoreStagedPolicy,
67
+ agentCorePolicyName,
68
+ AGENTCORE_NAME_PATTERN,
69
+ AGENTCORE_STATEMENT_MIN,
70
+ AGENTCORE_STATEMENT_MAX,
71
+ } from "./agentcore/embed";
72
+ export type {
73
+ AgentCoreCedarDefinition,
74
+ AgentCoreEmbedOptions,
75
+ AgentCoreLanguageDefinition,
76
+ AgentCorePolicyDefinition,
77
+ AgentCorePolicyResource,
78
+ AgentCorePolicySource,
79
+ AgentCoreStagedPolicy,
80
+ } from "./agentcore/embed";
81
+ export {
82
+ AGENTCORE_ENFORCEMENT,
83
+ describeStage,
84
+ enforcementMode,
85
+ enforcementStage,
86
+ isEnforcing,
87
+ } from "./agentcore/enforcement";
88
+ export type { AgentCoreEnforcementMode, AgentCoreStage } from "./agentcore/enforcement";
89
+ export { embeddedAgentCoreDefinitions, embeddedAgentCorePolicyStatements } from "./agentcore/scan";
90
+ export type { EmbeddedAgentCoreDefinition, EmbeddedAgentCoreStatement } from "./agentcore/scan";
91
+
92
+ // The dogwood temporal dialect (#1658) — typed temporal builders, the `.dw`
93
+ // serializer leg, `.dwschema` event schemas. Pre-release; see ./dogwood.
94
+ //
95
+ // Namespaced rather than flattened: the temporal sub-language's builders are
96
+ // named after its own grammar (`and`, `not`, `count`, `sum`, `str`, `field`,
97
+ // `record`, `window`), and spilling those into the package root would collide
98
+ // with a schema-generated entity class the moment someone declares one. The
99
+ // three entity classes are re-exported flat because that is what a policy file
100
+ // names.
101
+ export * as dogwood from "./dogwood/index";
102
+ export {
103
+ DOGWOOD_EVENT_SCHEMA_FILENAME,
104
+ DOGWOOD_EVENT_SCHEMA_TYPE,
105
+ DOGWOOD_MACRO_FILENAME,
106
+ DOGWOOD_MACRO_LIBRARY_TYPE,
107
+ DOGWOOD_POLICY_FILENAME,
108
+ DOGWOOD_POLICY_TYPE,
109
+ TemporalEventSchema,
110
+ TemporalMacroLibrary,
111
+ TemporalPolicy,
112
+ } from "./dogwood/policy";
113
+ export type { EventSchemaProps, MacroLibraryProps, TemporalPolicyProps } from "./dogwood/policy";
114
+ export { DOGWOOD_UPSTREAM } from "./dogwood/upstream";
115
+
116
+ // The replay Op composite and its typed step builders (#1661). Flat, like
117
+ // fly's `flyDeploy`: an Op factory is what a project's `ops/*.op.ts` names,
118
+ // and it carries no dependency on the temporal lexicon — see
119
+ // ./dogwood/replay-op.ts for why the composite ships from cedar.
120
+ export {
121
+ DEFAULT_REPLAY_REPORT_PATH,
122
+ PolicyReplayOp,
123
+ dogwoodReplayReportStep,
124
+ dogwoodReplayStep,
125
+ } from "./dogwood/replay-op";
126
+ export type {
127
+ DogwoodReplayReportStepOpts,
128
+ DogwoodReplayStepOpts,
129
+ PolicyReplayOpConfig,
130
+ PolicyReplayOpResources,
131
+ } from "./dogwood/replay-op";
132
+
57
133
  // Lint rules
58
134
  export { rules as cedarLintRules } from "./lint/rules";
59
135
 
@@ -127,4 +127,68 @@ export const cedarAuditCatalog: Record<string, RuleMeta> = {
127
127
  'Name the actions the grant needs (action == Action::"read", or action in [ … ]) so it does not widen as the schema gains actions.',
128
128
  { category: "security" },
129
129
  ),
130
+
131
+ // ── The dogwood dialect (DWD) ──────────────────────────────────
132
+ //
133
+ // A second id family under the same lexicon, declared on the serializer as
134
+ // `extraRulePrefixes` (#1349's mechanism, the way k8s owns ARGO and FLUX).
135
+ // Dogwood is a dialect of Cedar rather than a peer language — a `.dw` file
136
+ // stripped of Cedar semantics is meaningless — so its checks ship here, and
137
+ // its ids are their own family so a reader can tell which surface bit them.
138
+ DWDC010: auditRule(
139
+ "DWDC010",
140
+ "merge-worthy",
141
+ "guidance",
142
+ "Dogwood temporal predicate names an undeclared event kind",
143
+ "Declare the event kind in the emitted .dwschema, or correct the predicate to a kind the schema derives — upstream rejects the policy set otherwise.",
144
+ { category: "correctness" },
145
+ ),
146
+ DWDC011: auditRule(
147
+ "DWDC011",
148
+ "merge-worthy",
149
+ "guidance",
150
+ "Dogwood temporal window exceeds the event schema's max_window",
151
+ "Shorten the window, or raise max_window in the event schema. With no schema emitted the cap is upstream's 24h default.",
152
+ { category: "correctness" },
153
+ ),
154
+ DWDC012: auditRule(
155
+ "DWDC012",
156
+ "merge-worthy",
157
+ "guidance",
158
+ "Dogwood formerly/previous/since is missing its window",
159
+ "Give the operator a `within <n><s|m|h|d>` window — all three past-only operators require one. The typed builders take it as an argument.",
160
+ { category: "correctness" },
161
+ ),
162
+ DWDC013: auditRule(
163
+ "DWDC013",
164
+ "report-only",
165
+ "guidance",
166
+ "AgentCore policy embeds temporal text with no event schema emitted",
167
+ "Declare a TemporalEventSchema so the .dwschema ships with the policy, or record where the service schema is registered — AWS::BedrockAgentCore::Policy has no property to carry it, and until the engine has it every temporal predicate in the statement matches nothing.",
168
+ { category: "correctness" },
169
+ ),
170
+ DWDE010: auditRule(
171
+ "DWDE010",
172
+ "merge-worthy",
173
+ "guidance",
174
+ "Dogwood policy set fails `dogwood validate`",
175
+ "Fix what upstream's frontend names — a macro, a temporal type error, or a Cedar body the action schema rejects. If the finding is the advisory, install the dogwood binary (or set cedar.dogwood.binary) so full .dw validation can run at all; without it only the DWDC walls apply.",
176
+ { category: "correctness" },
177
+ ),
178
+ DWDE011: auditRule(
179
+ "DWDE011",
180
+ "merge-worthy",
181
+ "guidance",
182
+ "Cedar lowered from a dogwood policy set fails Cedar validation",
183
+ "Fix the entity type, action, or attribute Cedar's validator names in the lowered body — the temporal clauses became context slots, so everything the finding points at is ordinary Cedar the policy wrote.",
184
+ { category: "correctness" },
185
+ ),
186
+ DWDS010: auditRule(
187
+ "DWDS010",
188
+ "report-only",
189
+ "guidance",
190
+ "Dogwood event schema pins nothing, widening every temporal predicate",
191
+ "Pin a field to the deciding request (pin callerPrincipal: principalType(A) = principal), or record that cross-principal correlation is the intent — supplying any event schema opts out of upstream's default pin.",
192
+ { category: "security" },
193
+ ),
130
194
  };
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Turning a build's emitted files into `dogwood` CLI invocations (#1659).
3
+ *
4
+ * The DWDE checks both need the same thing: which of the emitted `.dw` files
5
+ * is a policy set, which is a macro library, which `.cedarschema` backs
6
+ * `--policy-schema`, and which `.dwschema` backs `--event-schema`. Planning
7
+ * that once here keeps the two checks from disagreeing about what they ran —
8
+ * DWDE010 validates a bundle and DWDE011 lowers the *same* bundle, and a
9
+ * finding from one that names different inputs than the other would be a
10
+ * puzzle rather than a report.
11
+ *
12
+ * Excluded from check auto-discovery by the "helper" filename filter.
13
+ */
14
+ import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
15
+ import { blankComments, dogwoodPolicyFiles, dogwoodSchemaFiles, type DogwoodArtifact } from "../../dogwood/scan";
16
+ import type { DogwoodBundle } from "../../dogwood/cli";
17
+
18
+ /** One planned invocation: the files, and the names to blame in a finding. */
19
+ export interface PreparedBundle {
20
+ lexicon: string;
21
+ /** The `.dw` policy set filename. */
22
+ source: string;
23
+ /** The `.cedarschema` filename passed as `--policy-schema`. */
24
+ policySchemaSource: string;
25
+ /** The `.dwschema` filename passed as `--event-schema`, when one was emitted. */
26
+ eventSchemaSource?: string;
27
+ bundle: DogwoodBundle;
28
+ }
29
+
30
+ /** What the CLI-gated checks should do with this build. */
31
+ export interface DogwoodRunPlan {
32
+ /** True when the build emitted a `.dw` policy set at all. */
33
+ hasPolicies: boolean;
34
+ /** Every invocation to make. Empty when {@link blocked} is set. */
35
+ bundles: PreparedBundle[];
36
+ /** Why nothing can be run, when policies exist but a required input does not. */
37
+ blocked?: string;
38
+ /** The lexicon the `.dw` output came from, for a finding that names no file. */
39
+ lexicon?: string;
40
+ }
41
+
42
+ /**
43
+ * A `.dw` file holding only `def` declarations is a macro library, not a
44
+ * policy set.
45
+ *
46
+ * By structure rather than by filename: the serializer's default is
47
+ * `macros.dw`, but `MacroLibraryProps.filename` overrides it, and `chant
48
+ * audit` runs over trees chant never wrote. A policy set is a file with a
49
+ * `permit` or `forbid` head in it — upstream's policy grammar has no third
50
+ * effect — and a set with inlined macros still has one.
51
+ */
52
+ export function looksLikeMacroLibrary(text: string): boolean {
53
+ return !/\b(permit|forbid)\s*\(/.test(blankComments(text));
54
+ }
55
+
56
+ function byName(a: DogwoodArtifact, b: DogwoodArtifact): number {
57
+ return a.source.localeCompare(b.source);
58
+ }
59
+
60
+ /**
61
+ * The Cedar action schema the CLI needs, as text.
62
+ *
63
+ * `--policy-schema` reads a human-readable `.cedarschema`; a `.cedarschema.json`
64
+ * is not a substitute, so it is not offered as one. Sorted by filename so a
65
+ * build emitting several picks the same one on every run.
66
+ */
67
+ function findPolicySchema(ctx: PostSynthContext): DogwoodArtifact | undefined {
68
+ const found: DogwoodArtifact[] = [];
69
+ for (const [lexicon, output] of ctx.outputs) {
70
+ if (typeof output === "string") continue;
71
+ for (const [source, text] of Object.entries(output.files ?? {})) {
72
+ if (typeof text !== "string") continue;
73
+ if (!source.endsWith(".cedarschema")) continue;
74
+ found.push({ lexicon, source, text });
75
+ }
76
+ }
77
+ return found.sort(byName)[0];
78
+ }
79
+
80
+ /**
81
+ * Plan every `dogwood` invocation this build calls for.
82
+ *
83
+ * One bundle per policy set per emitted event schema. A build with several
84
+ * `.dwschema` files means several services, and `--event-schema` takes exactly
85
+ * one file, so the set is run against each and every finding names the schema
86
+ * it was found under — the alternative is picking one silently and reporting
87
+ * against a schema the reader did not choose. Macro libraries do combine: a
88
+ * library is a sequence of `def`s, so several concatenate into the one file
89
+ * `--macros` accepts.
90
+ */
91
+ export function planDogwoodRuns(ctx: PostSynthContext): DogwoodRunPlan {
92
+ const files = dogwoodPolicyFiles(ctx).sort(byName);
93
+ const policySets = files.filter((f) => !looksLikeMacroLibrary(f.text));
94
+ if (policySets.length === 0) return { hasPolicies: false, bundles: [] };
95
+
96
+ const lexicon = policySets[0].lexicon;
97
+ const libraries = files.filter((f) => looksLikeMacroLibrary(f.text));
98
+ const macros = libraries.length > 0 ? libraries.map((f) => f.text).join("\n") : undefined;
99
+
100
+ const policySchema = findPolicySchema(ctx);
101
+ if (!policySchema) {
102
+ return {
103
+ hasPolicies: true,
104
+ bundles: [],
105
+ lexicon,
106
+ blocked:
107
+ "no Cedar action schema (.cedarschema) was emitted beside them, and `dogwood validate` requires one via --policy-schema",
108
+ };
109
+ }
110
+
111
+ const eventSchemas = dogwoodSchemaFiles(ctx).sort(byName);
112
+ const bundles: PreparedBundle[] = [];
113
+
114
+ for (const set of policySets) {
115
+ for (const events of eventSchemas.length > 0 ? eventSchemas : [undefined]) {
116
+ bundles.push({
117
+ lexicon: set.lexicon,
118
+ source: set.source,
119
+ policySchemaSource: policySchema.source,
120
+ ...(events ? { eventSchemaSource: events.source } : {}),
121
+ bundle: {
122
+ policies: set.text,
123
+ policySchema: policySchema.text,
124
+ ...(events ? { eventSchema: events.text } : {}),
125
+ ...(macros !== undefined ? { macros } : {}),
126
+ },
127
+ });
128
+ }
129
+ }
130
+
131
+ return { hasPolicies: true, bundles, lexicon };
132
+ }
133
+
134
+ /** How a finding names the inputs a run was made with. */
135
+ export function describeBundle(prepared: PreparedBundle): string {
136
+ const parts = [`"${prepared.source}"`, `against "${prepared.policySchemaSource}"`];
137
+ if (prepared.eventSchemaSource) parts.push(`with "${prepared.eventSchemaSource}"`);
138
+ return parts.join(" ");
139
+ }
@@ -0,0 +1,374 @@
1
+ /**
2
+ * The DWD walls.
3
+ *
4
+ * Each check reads the emitted text, so every case here is built as a
5
+ * `PostSynthContext` holding files rather than by driving the serializer —
6
+ * that is the same shape `chant audit` hands them when it runs over a
7
+ * checked-in `.dw` chant never wrote.
8
+ */
9
+ import { describe, test, expect } from "vitest";
10
+ import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
11
+ import type { SerializerResult } from "@intentius/chant/serializer";
12
+ import { postSynthChecks } from "./index";
13
+ import { dwdc010 } from "./dwdc010";
14
+ import { dwdc011 } from "./dwdc011";
15
+ import { dwdc012 } from "./dwdc012";
16
+ import { dwdc013 } from "./dwdc013";
17
+ import { dwds010 } from "./dwds010";
18
+ import { cedarAuditCatalog } from "../audit-catalog";
19
+ import { renderEventSchema, defaultEventSchema } from "../../dogwood/event-schema";
20
+ import { blankComments, temporalRegions } from "../../dogwood/scan";
21
+
22
+ function ctxOf(files: Record<string, string>): PostSynthContext {
23
+ const output: SerializerResult = { primary: "", files };
24
+ const outputs = new Map<string, string | SerializerResult>([["cedar", output]]);
25
+ return {
26
+ outputs,
27
+ entities: new Map(),
28
+ buildResult: { outputs, entities: new Map(), warnings: [], errors: [], sourceFileCount: 1 },
29
+ };
30
+ }
31
+
32
+ const PINNED = renderEventSchema(defaultEventSchema());
33
+
34
+ function policy(body: string): string {
35
+ return `@id("p")\npermit (\n principal,\n action == Ns::Action::"Read",\n resource\n)\nwhen temporal {\n ${body}\n}\n;\n`;
36
+ }
37
+
38
+ // ── DWDC010 ────────────────────────────────────────────────────────
39
+
40
+ describe("DWDC010 — the event kind must be declared", () => {
41
+ test("a declared kind passes", () => {
42
+ const ctx = ctxOf({
43
+ "policies.dw": policy('formerly within 1h Ns::Action::"Login"::response{}'),
44
+ "events.dwschema": PINNED,
45
+ });
46
+ expect(dwdc010.check(ctx)).toEqual([]);
47
+ });
48
+
49
+ test("an undeclared kind is an error naming both the predicate and the declared set", () => {
50
+ const ctx = ctxOf({
51
+ "policies.dw": policy('formerly within 1h Ns::Action::"Login"::nosuchkind{}'),
52
+ "events.dwschema": PINNED,
53
+ });
54
+ const [finding, ...rest] = dwdc010.check(ctx);
55
+ expect(rest).toEqual([]);
56
+ expect(finding.severity).toBe("error");
57
+ expect(finding.message).toContain('Ns::Action::"Login"::nosuchkind');
58
+ expect(finding.message).toContain("error, request, response");
59
+ expect(finding.entity).toBe("policies.dw");
60
+ });
61
+
62
+ test("author-defined kinds pass against the schema that declares them", () => {
63
+ const custom = "decision event <A>::attempt {\n ...inputs(A),\n}\n";
64
+ const ctx = ctxOf({
65
+ "policies.dw": policy('formerly within 1h Ns::Action::"Login"::attempt{}'),
66
+ "events.dwschema": custom,
67
+ });
68
+ expect(dwdc010.check(ctx)).toEqual([]);
69
+ });
70
+
71
+ test("silent when no schema was emitted — the far end's defaults decide", () => {
72
+ const ctx = ctxOf({ "policies.dw": policy('formerly within 1h Ns::Action::"Login"::nosuchkind{}') });
73
+ expect(dwdc010.check(ctx)).toEqual([]);
74
+ });
75
+
76
+ test("a commented-out predicate is not a finding", () => {
77
+ const ctx = ctxOf({
78
+ "policies.dw": `// formerly within 1h Ns::Action::"Login"::nosuchkind{}\n${policy('formerly within 1h Ns::Action::"Login"::request{}')}`,
79
+ "events.dwschema": PINNED,
80
+ });
81
+ expect(dwdc010.check(ctx)).toEqual([]);
82
+ });
83
+
84
+ test("a Cedar when clause is not temporal source, so it is not scanned", () => {
85
+ const cedarOnly = `@id("p")\npermit (\n principal,\n action,\n resource\n)\nwhen { context.kind == "nosuchkind" };\n`;
86
+ const ctx = ctxOf({ "policies.dw": cedarOnly, "events.dwschema": PINNED });
87
+ expect(dwdc010.check(ctx)).toEqual([]);
88
+ });
89
+
90
+ test("the same predicate twice is reported once", () => {
91
+ const body = 'formerly within 1h Ns::Action::"Login"::bogus{} && previous within 1h Ns::Action::"Login"::bogus{}';
92
+ const ctx = ctxOf({ "policies.dw": policy(body), "events.dwschema": PINNED });
93
+ expect(dwdc010.check(ctx)).toHaveLength(1);
94
+ });
95
+ });
96
+
97
+ // ── DWDC011 ────────────────────────────────────────────────────────
98
+
99
+ describe("DWDC011 — the window must fit inside max_window", () => {
100
+ test("a window inside the default 24h cap passes with no schema emitted", () => {
101
+ const ctx = ctxOf({ "policies.dw": policy('formerly within 1h Ns::Action::"Login"::request{}') });
102
+ expect(dwdc011.check(ctx)).toEqual([]);
103
+ });
104
+
105
+ test("48h under the default 24h cap is an error even with no schema", () => {
106
+ const ctx = ctxOf({ "policies.dw": policy('formerly within 48h Ns::Action::"Login"::request{}') });
107
+ const [finding] = dwdc011.check(ctx);
108
+ expect(finding.severity).toBe("error");
109
+ expect(finding.message).toContain("looks back 48h");
110
+ expect(finding.message).toContain("24h");
111
+ expect(finding.message).toContain("no event schema was emitted");
112
+ });
113
+
114
+ test("raising max_window in the schema licenses the longer window", () => {
115
+ const raised = `max_window = 30d\n\n${PINNED.split("\n").slice(3).join("\n")}`;
116
+ const ctx = ctxOf({
117
+ "policies.dw": policy('formerly within 7d Ns::Action::"Login"::request{}'),
118
+ "events.dwschema": raised,
119
+ });
120
+ expect(dwdc011.check(ctx)).toEqual([]);
121
+ });
122
+
123
+ test("an emitted schema with no directive still caps at 24h", () => {
124
+ const ctx = ctxOf({
125
+ "policies.dw": policy('formerly within 25h Ns::Action::"Login"::request{}'),
126
+ "events.dwschema": PINNED,
127
+ });
128
+ expect(dwdc011.check(ctx)).toHaveLength(1);
129
+ });
130
+
131
+ test("the tightest of several schemas wins", () => {
132
+ const raised = `max_window = 30d\n\n${PINNED.split("\n").slice(3).join("\n")}`;
133
+ const ctx = ctxOf({
134
+ "policies.dw": policy('formerly within 7d Ns::Action::"Login"::request{}'),
135
+ "events.dwschema": raised,
136
+ "gateway.dwschema": PINNED,
137
+ });
138
+ expect(dwdc011.check(ctx)).toHaveLength(1);
139
+ });
140
+
141
+ test("a macro-call interval counts — it is the window the body resolves to", () => {
142
+ const ctx = ctxOf({ "policies.dw": policy('once(48h, Ns::Action::"Login"::request{})') });
143
+ const [finding] = dwdc011.check(ctx);
144
+ expect(finding.message).toContain("looks back 48h");
145
+ });
146
+
147
+ test("a window inside a macro definition is scanned too", () => {
148
+ const dw = `def temporal wide(?s) {\n formerly within 72h ?s\n};\n\n${policy('wide(Ns::Action::"Login"::request{})')}`;
149
+ const ctx = ctxOf({ "policies.dw": dw });
150
+ expect(dwdc011.check(ctx)).toHaveLength(1);
151
+ });
152
+
153
+ test("no policy file means nothing to judge", () => {
154
+ expect(dwdc011.check(ctxOf({ "events.dwschema": PINNED }))).toEqual([]);
155
+ });
156
+ });
157
+
158
+ // ── DWDC012 ────────────────────────────────────────────────────────
159
+
160
+ describe("DWDC012 — the window is not optional", () => {
161
+ test("a windowed operator passes", () => {
162
+ const ctx = ctxOf({ "policies.dw": policy('formerly within 1h Ns::Action::"Login"::request{}') });
163
+ expect(dwdc012.check(ctx)).toEqual([]);
164
+ });
165
+
166
+ test("a bare formerly is an error", () => {
167
+ const ctx = ctxOf({ "policies.dw": policy('formerly Ns::Action::"Login"::request{}') });
168
+ const [finding] = dwdc012.check(ctx);
169
+ expect(finding.severity).toBe("error");
170
+ expect(finding.message).toContain("`formerly` with no `within` window");
171
+ });
172
+
173
+ test("previous and since are held to the same rule", () => {
174
+ for (const operator of ["previous", "since"]) {
175
+ const ctx = ctxOf({ "policies.dw": policy(`${operator} Ns::Action::"Login"::request{}`) });
176
+ expect(dwdc012.check(ctx)[0]?.message).toContain(`\`${operator}\``);
177
+ }
178
+ });
179
+
180
+ test("a `within ?w` in a macro body is a window", () => {
181
+ const dw = `def temporal once(?w, ?s) {\n formerly within ?w ?s\n};\n`;
182
+ expect(dwdc012.check(ctxOf({ "policies.dw": dw }))).toEqual([]);
183
+ });
184
+
185
+ test("a Cedar attribute named `since` outside a temporal block is not the operator", () => {
186
+ const cedarOnly = `@id("p")\npermit (\n principal,\n action,\n resource\n)\nwhen { context.since > 3 };\n`;
187
+ expect(dwdc012.check(ctxOf({ "policies.dw": cedarOnly }))).toEqual([]);
188
+ });
189
+ });
190
+
191
+ // ── DWDS010 ────────────────────────────────────────────────────────
192
+
193
+ describe("DWDS010 — an unpinned schema widens every predicate", () => {
194
+ test("the default shape's pin satisfies it", () => {
195
+ expect(dwds010.check(ctxOf({ "events.dwschema": PINNED }))).toEqual([]);
196
+ });
197
+
198
+ test("a schema with no pin is a report-only warning", () => {
199
+ const unpinned = renderEventSchema(defaultEventSchema({ pinCallerPrincipal: false }));
200
+ const [finding] = dwds010.check(ctxOf({ "events.dwschema": unpinned }));
201
+ expect(finding.severity).toBe("warning");
202
+ expect(finding.message).toContain("declares no pinned field");
203
+ expect(finding.entity).toBe("events.dwschema");
204
+ });
205
+
206
+ test("a file with no declarations at all is not the pin's problem", () => {
207
+ expect(dwds010.check(ctxOf({ "events.dwschema": "// nothing here\n" }))).toEqual([]);
208
+ });
209
+ });
210
+
211
+ // ── DWDC013 ────────────────────────────────────────────────────────
212
+
213
+ /**
214
+ * A build with two lexicons in it: the cedar one, and whatever emitted the
215
+ * CloudFormation the AgentCore policy landed in. The check reads the emitted
216
+ * JSON rather than an aws entity class, which is what lets the cedar lexicon
217
+ * have an opinion about an embedding without depending on the aws lexicon.
218
+ */
219
+ function ctxOfLexicons(byLexicon: Record<string, Record<string, string>>): PostSynthContext {
220
+ const outputs = new Map<string, string | SerializerResult>();
221
+ for (const [lexicon, files] of Object.entries(byLexicon)) {
222
+ outputs.set(lexicon, { primary: "", files } satisfies SerializerResult);
223
+ }
224
+ return {
225
+ outputs,
226
+ entities: new Map(),
227
+ buildResult: { outputs, entities: new Map(), warnings: [], errors: [], sourceFileCount: 1 },
228
+ };
229
+ }
230
+
231
+ const TEMPORAL_STATEMENT =
232
+ '@id("write-needs-approval")\npermit (\n principal,\n action == Ns::Action::"Write",\n resource\n)\nwhen temporal {\n formerly within 1h Ns::Action::"Approve"::response{}\n}\n;';
233
+
234
+ const PLAIN_STATEMENT = '@id("deny")\nforbid (\n principal,\n action,\n resource\n)\nunless { context.ok == true };';
235
+
236
+ function template(definition: Record<string, unknown>, logicalId = "GatewayPolicy"): string {
237
+ return JSON.stringify({
238
+ Resources: {
239
+ [logicalId]: {
240
+ Type: "AWS::BedrockAgentCore::Policy",
241
+ Properties: {
242
+ PolicyEngineId: "GatewayEngine-abcdefghij",
243
+ Name: logicalId,
244
+ Definition: definition,
245
+ EnforcementMode: "LOG_ONLY",
246
+ },
247
+ },
248
+ },
249
+ });
250
+ }
251
+
252
+ describe("DWDC013 — an embedded temporal statement needs its event schema", () => {
253
+ test("temporal text with no emitted schema is a warning naming the resource", () => {
254
+ const ctx = ctxOfLexicons({
255
+ cedar: { "policies.dw": policy('formerly within 1h Ns::Action::"Approve"::response{}') },
256
+ aws: { "template.json": template({ Policy: { Statement: TEMPORAL_STATEMENT } }) },
257
+ });
258
+
259
+ const [finding, ...rest] = dwdc013.check(ctx);
260
+ expect(rest).toEqual([]);
261
+ expect(finding.severity).toBe("warning");
262
+ expect(finding.entity).toBe("GatewayPolicy");
263
+ expect(finding.lexicon).toBe("aws");
264
+ expect(finding.message).toContain("Definition.Policy.Statement");
265
+ expect(finding.message).toContain("no .dwschema");
266
+ });
267
+
268
+ test("an emitted event schema settles it", () => {
269
+ const ctx = ctxOfLexicons({
270
+ cedar: { "events.dwschema": PINNED },
271
+ aws: { "template.json": template({ Policy: { Statement: TEMPORAL_STATEMENT } }) },
272
+ });
273
+ expect(dwdc013.check(ctx)).toEqual([]);
274
+ });
275
+
276
+ test("plain Cedar in the language-agnostic arm is not this check's business", () => {
277
+ const ctx = ctxOfLexicons({
278
+ aws: { "template.json": template({ Policy: { Statement: PLAIN_STATEMENT } }) },
279
+ });
280
+ expect(dwdc013.check(ctx)).toEqual([]);
281
+ });
282
+
283
+ test("the Cedar arm is left alone — a temporal statement there is a different bug", () => {
284
+ const ctx = ctxOfLexicons({
285
+ aws: { "template.json": template({ Cedar: { Statement: TEMPORAL_STATEMENT } }) },
286
+ });
287
+ expect(dwdc013.check(ctx)).toEqual([]);
288
+ });
289
+
290
+ test("every embedded policy is reported, not only the first", () => {
291
+ const both = JSON.stringify({
292
+ Resources: {
293
+ Approval: {
294
+ Type: "AWS::BedrockAgentCore::Policy",
295
+ Properties: { Definition: { Policy: { Statement: TEMPORAL_STATEMENT } } },
296
+ },
297
+ Budget: {
298
+ Type: "AWS::BedrockAgentCore::Policy",
299
+ Properties: { Definition: { Policy: { Statement: TEMPORAL_STATEMENT } } },
300
+ },
301
+ },
302
+ });
303
+ const findings = dwdc013.check(ctxOfLexicons({ aws: { "template.json": both } }));
304
+ expect(findings.map((f) => f.entity).sort()).toEqual(["Approval", "Budget"]);
305
+ });
306
+
307
+ test("a build with no embedding at all is silent", () => {
308
+ const ctx = ctxOfLexicons({
309
+ cedar: { "policies.dw": policy('formerly within 1h Ns::Action::"Approve"::response{}') },
310
+ });
311
+ expect(dwdc013.check(ctx)).toEqual([]);
312
+ });
313
+
314
+ test("non-JSON output is skipped rather than reported", () => {
315
+ const ctx = ctxOfLexicons({ aws: { "template.yaml": "Resources:\n Gateway:\n Type: Whatever\n" } });
316
+ expect(dwdc013.check(ctx)).toEqual([]);
317
+ });
318
+ });
319
+
320
+ // ── Registration ───────────────────────────────────────────────────
321
+
322
+ describe("registration", () => {
323
+ test("every DWD check is auto-discovered into the committed barrel", () => {
324
+ const ids = postSynthChecks.map((c) => c.id).filter((id) => id.startsWith("DWD"));
325
+ expect(ids.sort()).toEqual([
326
+ "DWDC010",
327
+ "DWDC011",
328
+ "DWDC012",
329
+ "DWDC013",
330
+ "DWDE010",
331
+ "DWDE011",
332
+ "DWDS010",
333
+ ]);
334
+ });
335
+
336
+ test("every DWD check has a catalog entry, so `chant audit` can title it", () => {
337
+ for (const check of postSynthChecks) {
338
+ if (!check.id.startsWith("DWD")) continue;
339
+ const meta = cedarAuditCatalog[check.id];
340
+ expect(meta, `${check.id} is catalogued`).toBeDefined();
341
+ expect(meta.title.length).toBeGreaterThan(0);
342
+ expect(meta.remediation.length).toBeGreaterThan(0);
343
+ }
344
+ });
345
+
346
+ test("the catalog carries no DWD entry without a check behind it", () => {
347
+ const shipped = new Set(postSynthChecks.map((c) => c.id));
348
+ const stale = Object.keys(cedarAuditCatalog).filter((id) => id.startsWith("DWD") && !shipped.has(id));
349
+ expect(stale).toEqual([]);
350
+ });
351
+ });
352
+
353
+ // ── The scanner itself ─────────────────────────────────────────────
354
+
355
+ describe("the text scanner", () => {
356
+ test("a // inside a string literal is not a comment", () => {
357
+ const text = 'when temporal { Ns::Action::"a//b"::request{} }';
358
+ expect(blankComments(text)).toBe(text);
359
+ });
360
+
361
+ test("a comment is blanked without moving any offset", () => {
362
+ const text = 'x // hidden\ny';
363
+ const blanked = blankComments(text);
364
+ expect(blanked).toHaveLength(text.length);
365
+ expect(blanked).toBe("x \ny");
366
+ });
367
+
368
+ test("a temporal block is extracted through its nested braces", () => {
369
+ const text = 'when temporal { formerly within 1h Ns::Action::"A"::request{ input.x: 1 } }';
370
+ expect(temporalRegions(text)).toEqual([
371
+ ' formerly within 1h Ns::Action::"A"::request{ input.x: 1 } ',
372
+ ]);
373
+ });
374
+ });