@intentius/chant-lexicon-cedar 0.44.8

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 (265) hide show
  1. package/README.md +190 -0
  2. package/dist/avp/ambient.d.ts +54 -0
  3. package/dist/avp/ambient.d.ts.map +1 -0
  4. package/dist/avp/client.d.ts +127 -0
  5. package/dist/avp/client.d.ts.map +1 -0
  6. package/dist/avp/describe-resources.d.ts +42 -0
  7. package/dist/avp/describe-resources.d.ts.map +1 -0
  8. package/dist/avp/embed.d.ts +120 -0
  9. package/dist/avp/embed.d.ts.map +1 -0
  10. package/dist/avp/live-export.d.ts +94 -0
  11. package/dist/avp/live-export.d.ts.map +1 -0
  12. package/dist/avp/ownership.d.ts +97 -0
  13. package/dist/avp/ownership.d.ts.map +1 -0
  14. package/dist/avp/statement.d.ts +22 -0
  15. package/dist/avp/statement.d.ts.map +1 -0
  16. package/dist/avp/store.d.ts +82 -0
  17. package/dist/avp/store.d.ts.map +1 -0
  18. package/dist/avp/testdata/mock-transport.d.ts +55 -0
  19. package/dist/avp/testdata/mock-transport.d.ts.map +1 -0
  20. package/dist/codegen/docs-cli.d.ts +3 -0
  21. package/dist/codegen/docs-cli.d.ts.map +1 -0
  22. package/dist/codegen/docs.d.ts +26 -0
  23. package/dist/codegen/docs.d.ts.map +1 -0
  24. package/dist/codegen/emit.d.ts +85 -0
  25. package/dist/codegen/emit.d.ts.map +1 -0
  26. package/dist/codegen/generate-cli.d.ts +3 -0
  27. package/dist/codegen/generate-cli.d.ts.map +1 -0
  28. package/dist/codegen/generate.d.ts +37 -0
  29. package/dist/codegen/generate.d.ts.map +1 -0
  30. package/dist/codegen/naming.d.ts +48 -0
  31. package/dist/codegen/naming.d.ts.map +1 -0
  32. package/dist/codegen/package.d.ts +10 -0
  33. package/dist/codegen/package.d.ts.map +1 -0
  34. package/dist/composites/deny-by-default-set.d.ts +58 -0
  35. package/dist/composites/deny-by-default-set.d.ts.map +1 -0
  36. package/dist/composites/index.d.ts +12 -0
  37. package/dist/composites/index.d.ts.map +1 -0
  38. package/dist/composites/owner-can-manage.d.ts +48 -0
  39. package/dist/composites/owner-can-manage.d.ts.map +1 -0
  40. package/dist/config.d.ts +102 -0
  41. package/dist/config.d.ts.map +1 -0
  42. package/dist/coverage.d.ts +59 -0
  43. package/dist/coverage.d.ts.map +1 -0
  44. package/dist/detect.d.ts +18 -0
  45. package/dist/detect.d.ts.map +1 -0
  46. package/dist/generated/index.d.ts +251 -0
  47. package/dist/generated/index.d.ts.map +1 -0
  48. package/dist/generated/runtime.d.ts +2 -0
  49. package/dist/generated/runtime.d.ts.map +1 -0
  50. package/dist/import/adapter.d.ts +21 -0
  51. package/dist/import/adapter.d.ts.map +1 -0
  52. package/dist/import/clause-text.d.ts +38 -0
  53. package/dist/import/clause-text.d.ts.map +1 -0
  54. package/dist/import/generator.d.ts +53 -0
  55. package/dist/import/generator.d.ts.map +1 -0
  56. package/dist/import/parser.d.ts +57 -0
  57. package/dist/import/parser.d.ts.map +1 -0
  58. package/dist/index.d.ts +34 -0
  59. package/dist/index.d.ts.map +1 -0
  60. package/dist/init-templates.d.ts +32 -0
  61. package/dist/init-templates.d.ts.map +1 -0
  62. package/dist/integrity.json +22 -0
  63. package/dist/lint/audit-catalog.d.ts +29 -0
  64. package/dist/lint/audit-catalog.d.ts.map +1 -0
  65. package/dist/lint/post-synth/cedar-helpers.d.ts +97 -0
  66. package/dist/lint/post-synth/cedar-helpers.d.ts.map +1 -0
  67. package/dist/lint/post-synth/cedc010.d.ts +17 -0
  68. package/dist/lint/post-synth/cedc010.d.ts.map +1 -0
  69. package/dist/lint/post-synth/cedc011.d.ts +17 -0
  70. package/dist/lint/post-synth/cedc011.d.ts.map +1 -0
  71. package/dist/lint/post-synth/cedc012.d.ts +21 -0
  72. package/dist/lint/post-synth/cedc012.d.ts.map +1 -0
  73. package/dist/lint/post-synth/cedc013.d.ts +20 -0
  74. package/dist/lint/post-synth/cedc013.d.ts.map +1 -0
  75. package/dist/lint/post-synth/cedc014.d.ts +19 -0
  76. package/dist/lint/post-synth/cedc014.d.ts.map +1 -0
  77. package/dist/lint/post-synth/cede010.d.ts +34 -0
  78. package/dist/lint/post-synth/cede010.d.ts.map +1 -0
  79. package/dist/lint/post-synth/cede011.d.ts +22 -0
  80. package/dist/lint/post-synth/cede011.d.ts.map +1 -0
  81. package/dist/lint/post-synth/ceds010.d.ts +24 -0
  82. package/dist/lint/post-synth/ceds010.d.ts.map +1 -0
  83. package/dist/lint/post-synth/ceds011.d.ts +19 -0
  84. package/dist/lint/post-synth/ceds011.d.ts.map +1 -0
  85. package/dist/lint/post-synth/ceds012.d.ts +17 -0
  86. package/dist/lint/post-synth/ceds012.d.ts.map +1 -0
  87. package/dist/lint/post-synth/index.d.ts +3 -0
  88. package/dist/lint/post-synth/index.d.ts.map +1 -0
  89. package/dist/lint/post-synth/wasm-helpers.d.ts +108 -0
  90. package/dist/lint/post-synth/wasm-helpers.d.ts.map +1 -0
  91. package/dist/lint/rules/index.d.ts +10 -0
  92. package/dist/lint/rules/index.d.ts.map +1 -0
  93. package/dist/lint/rules/policy-shape.d.ts +31 -0
  94. package/dist/lint/rules/policy-shape.d.ts.map +1 -0
  95. package/dist/lsp/completions.d.ts +24 -0
  96. package/dist/lsp/completions.d.ts.map +1 -0
  97. package/dist/lsp/hover.d.ts +11 -0
  98. package/dist/lsp/hover.d.ts.map +1 -0
  99. package/dist/lsp/registry.d.ts +45 -0
  100. package/dist/lsp/registry.d.ts.map +1 -0
  101. package/dist/manifest.json +8 -0
  102. package/dist/mcp/index.d.ts +34 -0
  103. package/dist/mcp/index.d.ts.map +1 -0
  104. package/dist/mcp/policy-coverage.d.ts +81 -0
  105. package/dist/mcp/policy-coverage.d.ts.map +1 -0
  106. package/dist/meta.json +699 -0
  107. package/dist/okf/index.md +37 -0
  108. package/dist/okf/rules/CEDC001.md +11 -0
  109. package/dist/okf/rules/CEDC010.md +11 -0
  110. package/dist/okf/rules/CEDC011.md +15 -0
  111. package/dist/okf/rules/CEDC012.md +11 -0
  112. package/dist/okf/rules/CEDC013.md +15 -0
  113. package/dist/okf/rules/CEDC014.md +15 -0
  114. package/dist/okf/rules/CEDE010.md +15 -0
  115. package/dist/okf/rules/CEDE011.md +15 -0
  116. package/dist/okf/rules/CEDS010.md +15 -0
  117. package/dist/okf/rules/CEDS011.md +11 -0
  118. package/dist/okf/rules/CEDS012.md +15 -0
  119. package/dist/okf/types/AdminAction.md +14 -0
  120. package/dist/okf/types/Application.md +14 -0
  121. package/dist/okf/types/ApproveAction.md +13 -0
  122. package/dist/okf/types/CommentAction.md +14 -0
  123. package/dist/okf/types/CreateAction.md +14 -0
  124. package/dist/okf/types/DeleteAction.md +14 -0
  125. package/dist/okf/types/Document.md +18 -0
  126. package/dist/okf/types/Folder.md +15 -0
  127. package/dist/okf/types/Group.md +14 -0
  128. package/dist/okf/types/ListAction.md +14 -0
  129. package/dist/okf/types/Policy.md +29 -0
  130. package/dist/okf/types/ReadAction.md +14 -0
  131. package/dist/okf/types/ServiceAccount.md +15 -0
  132. package/dist/okf/types/ShareAction.md +14 -0
  133. package/dist/okf/types/Team.md +14 -0
  134. package/dist/okf/types/User.md +18 -0
  135. package/dist/okf/types/WriteAction.md +14 -0
  136. package/dist/package-cli.d.ts +3 -0
  137. package/dist/package-cli.d.ts.map +1 -0
  138. package/dist/plugin.d.ts +8 -0
  139. package/dist/plugin.d.ts.map +1 -0
  140. package/dist/rules/cedar-helpers.ts +209 -0
  141. package/dist/rules/cedc010.ts +80 -0
  142. package/dist/rules/cedc011.ts +47 -0
  143. package/dist/rules/cedc012.ts +64 -0
  144. package/dist/rules/cedc013.ts +68 -0
  145. package/dist/rules/cedc014.ts +73 -0
  146. package/dist/rules/cede010.ts +89 -0
  147. package/dist/rules/cede011.ts +58 -0
  148. package/dist/rules/ceds010.ts +57 -0
  149. package/dist/rules/ceds011.ts +42 -0
  150. package/dist/rules/ceds012.ts +50 -0
  151. package/dist/rules/policy-shape.ts +148 -0
  152. package/dist/rules/wasm-helpers.ts +315 -0
  153. package/dist/serializer.d.ts +138 -0
  154. package/dist/serializer.d.ts.map +1 -0
  155. package/dist/spec/fetch.d.ts +71 -0
  156. package/dist/spec/fetch.d.ts.map +1 -0
  157. package/dist/spec/parse.d.ts +113 -0
  158. package/dist/spec/parse.d.ts.map +1 -0
  159. package/dist/spec/pin.d.ts +116 -0
  160. package/dist/spec/pin.d.ts.map +1 -0
  161. package/dist/spec/pinned-names.json +18 -0
  162. package/dist/spec/wasm.d.ts +143 -0
  163. package/dist/spec/wasm.d.ts.map +1 -0
  164. package/dist/types/index.d.ts +219 -0
  165. package/dist/validate-cli.d.ts +3 -0
  166. package/dist/validate-cli.d.ts.map +1 -0
  167. package/dist/validate.d.ts +25 -0
  168. package/dist/validate.d.ts.map +1 -0
  169. package/package.json +75 -0
  170. package/src/avp/OWNERSHIP.md +125 -0
  171. package/src/avp/ambient.test.ts +113 -0
  172. package/src/avp/ambient.ts +124 -0
  173. package/src/avp/client.ts +310 -0
  174. package/src/avp/describe-resources.test.ts +316 -0
  175. package/src/avp/describe-resources.ts +215 -0
  176. package/src/avp/embed.test.ts +114 -0
  177. package/src/avp/embed.ts +190 -0
  178. package/src/avp/live-export.test.ts +232 -0
  179. package/src/avp/live-export.ts +185 -0
  180. package/src/avp/ownership.test.ts +101 -0
  181. package/src/avp/ownership.ts +170 -0
  182. package/src/avp/statement.ts +50 -0
  183. package/src/avp/store.ts +152 -0
  184. package/src/avp/testdata/mock-transport.ts +147 -0
  185. package/src/codegen/docs-cli.ts +7 -0
  186. package/src/codegen/docs.ts +873 -0
  187. package/src/codegen/emit.ts +496 -0
  188. package/src/codegen/generate-cli.ts +18 -0
  189. package/src/codegen/generate.test.ts +128 -0
  190. package/src/codegen/generate.ts +123 -0
  191. package/src/codegen/naming.ts +101 -0
  192. package/src/codegen/package.ts +52 -0
  193. package/src/composites/composites.test.ts +206 -0
  194. package/src/composites/deny-by-default-set.ts +98 -0
  195. package/src/composites/index.ts +13 -0
  196. package/src/composites/owner-can-manage.ts +80 -0
  197. package/src/config-namespace.test.ts +44 -0
  198. package/src/config.test.ts +82 -0
  199. package/src/config.ts +119 -0
  200. package/src/coverage.test.ts +61 -0
  201. package/src/coverage.ts +166 -0
  202. package/src/detect.test.ts +64 -0
  203. package/src/detect.ts +59 -0
  204. package/src/generated/index.d.ts +219 -0
  205. package/src/generated/index.ts +279 -0
  206. package/src/generated/lexicon-cedar.json +699 -0
  207. package/src/generated/runtime.ts +2 -0
  208. package/src/import/adapter.ts +63 -0
  209. package/src/import/clause-text.ts +178 -0
  210. package/src/import/generator.test.ts +133 -0
  211. package/src/import/generator.ts +219 -0
  212. package/src/import/parser.test.ts +265 -0
  213. package/src/import/parser.ts +352 -0
  214. package/src/import/roundtrip.test.ts +127 -0
  215. package/src/import/testdata/full.cedar +36 -0
  216. package/src/import/testdata/full.cedar.json +238 -0
  217. package/src/import/testdata/realistic.cedar +35 -0
  218. package/src/import/testdata/simple.cedar +6 -0
  219. package/src/index.ts +94 -0
  220. package/src/init-templates.test.ts +158 -0
  221. package/src/init-templates.ts +358 -0
  222. package/src/lint/audit-catalog.ts +130 -0
  223. package/src/lint/post-synth/cedar-helpers.ts +209 -0
  224. package/src/lint/post-synth/cedc010.ts +80 -0
  225. package/src/lint/post-synth/cedc011.ts +47 -0
  226. package/src/lint/post-synth/cedc012.ts +64 -0
  227. package/src/lint/post-synth/cedc013.ts +68 -0
  228. package/src/lint/post-synth/cedc014.ts +73 -0
  229. package/src/lint/post-synth/cede010.ts +89 -0
  230. package/src/lint/post-synth/cede011.ts +58 -0
  231. package/src/lint/post-synth/ceds010.ts +57 -0
  232. package/src/lint/post-synth/ceds011.ts +42 -0
  233. package/src/lint/post-synth/ceds012.ts +50 -0
  234. package/src/lint/post-synth/index.ts +25 -0
  235. package/src/lint/post-synth/post-synth.test.ts +474 -0
  236. package/src/lint/post-synth/wasm-helpers.ts +315 -0
  237. package/src/lint/rules/index.ts +12 -0
  238. package/src/lint/rules/policy-shape.test.ts +90 -0
  239. package/src/lint/rules/policy-shape.ts +148 -0
  240. package/src/lsp/completions.test.ts +111 -0
  241. package/src/lsp/completions.ts +55 -0
  242. package/src/lsp/hover.test.ts +82 -0
  243. package/src/lsp/hover.ts +75 -0
  244. package/src/lsp/registry.ts +74 -0
  245. package/src/mcp/index.ts +103 -0
  246. package/src/mcp/policy-coverage.test.ts +176 -0
  247. package/src/mcp/policy-coverage.ts +205 -0
  248. package/src/package-cli.ts +22 -0
  249. package/src/plugin.test.ts +115 -0
  250. package/src/plugin.ts +312 -0
  251. package/src/serializer.test.ts +502 -0
  252. package/src/serializer.ts +335 -0
  253. package/src/skills/chant-cedar-authoring.md +180 -0
  254. package/src/skills/chant-cedar-avp-embedding.md +125 -0
  255. package/src/skills/chant-cedar-meta-policy.md +119 -0
  256. package/src/spec/default-schema.cedarschema +108 -0
  257. package/src/spec/fetch.ts +107 -0
  258. package/src/spec/parse.test.ts +123 -0
  259. package/src/spec/parse.ts +253 -0
  260. package/src/spec/pin.test.ts +88 -0
  261. package/src/spec/pin.ts +243 -0
  262. package/src/spec/pinned-names.json +18 -0
  263. package/src/spec/wasm.ts +283 -0
  264. package/src/validate-cli.ts +8 -0
  265. package/src/validate.ts +85 -0
@@ -0,0 +1,58 @@
1
+ /**
2
+ * CEDE011: surface the Cedar validator's warnings
3
+ *
4
+ * `validate()` returns two arrays, and the second one is the interesting one
5
+ * for a policy nobody has noticed is dead. The validator's warnings are the
6
+ * findings that do not make the set invalid but do mean it is not doing what it
7
+ * says: "policy is impossible: the policy expression evaluates to false for all
8
+ * valid requests" is the one that matters most — a guard whose attribute
9
+ * comparison can never hold, so the grant it wraps never fires and the access
10
+ * it was written to give quietly does not exist.
11
+ *
12
+ * Split from CEDE010 rather than folded into it because the two mean different
13
+ * things: an error is a policy no evaluator will accept, a warning is a policy
14
+ * that will be accepted and will not work. They deserve different severities
15
+ * and, in a report, different lines.
16
+ *
17
+ * Same two traps as CEDE010 — findings live under `type: "success"`, and their
18
+ * order is not stable, so `validatePolicySet` sorts before returning.
19
+ */
20
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
21
+ import { parsedPolicySets } from "./cedar-helpers";
22
+ import { findSchema, loadWasm, normalizePolicySet, validatePolicySet } from "./wasm-helpers";
23
+
24
+ export const cede011: PostSynthCheck = {
25
+ id: "CEDE011",
26
+ description: "Cedar validation warnings (impossible policies, shadowed conditions) on an emitted policy set",
27
+
28
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
29
+ const diagnostics: PostSynthDiagnostic[] = [];
30
+ const sets = parsedPolicySets(ctx);
31
+ if (sets.length === 0) return diagnostics;
32
+
33
+ // Both "no validator" and "no schema" are already reported once by
34
+ // CEDC010/CEDE010; repeating them here would triple the same line.
35
+ const wasm = loadWasm();
36
+ if (!wasm) return diagnostics;
37
+ const schema = findSchema(ctx);
38
+ if (!schema) return diagnostics;
39
+
40
+ for (const set of sets) {
41
+ const { policySet } = normalizePolicySet(wasm, set);
42
+ const outcome = validatePolicySet(wasm, policySet, schema.schema);
43
+ if (outcome.failure) continue; // CEDE010 reports the failed call
44
+
45
+ for (const finding of outcome.warnings) {
46
+ diagnostics.push({
47
+ checkId: "CEDE011",
48
+ severity: "warning",
49
+ message: `Policy "${finding.policyId}" validates but Cedar warns about it: ${finding.message}`,
50
+ entity: finding.policyId,
51
+ lexicon: set.lexicon,
52
+ });
53
+ }
54
+ }
55
+
56
+ return diagnostics;
57
+ },
58
+ };
@@ -0,0 +1,57 @@
1
+ /**
2
+ * CEDS010: a bare `permit(principal, action, resource);` is a standing grant
3
+ *
4
+ * The one policy that permits everything to everyone on everything. It is
5
+ * valid Cedar, it is what a scaffolded policy looks like before anyone has
6
+ * written the real one, and the Cedar validator says nothing about it — checked
7
+ * directly against 4.12.0 (chant #1648): a schema-clean run over exactly this
8
+ * policy returns zero errors, zero warnings, zero other warnings. There is no
9
+ * upstream tool that will catch it, which is why the epic names this wall as a
10
+ * definition-of-done item and why it lives here.
11
+ *
12
+ * The shape is unambiguous in the JSON: all three scopes unconstrained
13
+ * (`op: "All"`) and no `when`/`unless` clause to narrow it. Anything with even
14
+ * one constraint or one guard is somebody's deliberate decision and is not this
15
+ * check's business (an unconstrained *action* alone is CEDS012).
16
+ *
17
+ * Severity is env-gated on the same `ctx.env` seam the k8s org-policy example
18
+ * and the aws tier checks use: a production build fails, everything else warns,
19
+ * so the scaffold-and-iterate loop stays workable while the thing that ships
20
+ * cannot carry it.
21
+ */
22
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
23
+ import { conditionsOf, effectOf, isProdLikeEnv, parsedPolicySets, scopeIsAll } from "./cedar-helpers";
24
+
25
+ export const ceds010: PostSynthCheck = {
26
+ id: "CEDS010",
27
+ description: "A bare permit(principal, action, resource) with no scope constraints and no conditions grants everything",
28
+
29
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
30
+ const diagnostics: PostSynthDiagnostic[] = [];
31
+ const prod = isProdLikeEnv(ctx.env);
32
+ const severity = prod ? "error" : "warning";
33
+
34
+ for (const set of parsedPolicySets(ctx)) {
35
+ for (const entry of set.entries) {
36
+ if (effectOf(entry.policy) !== "permit") continue;
37
+ const policy = entry.policy;
38
+ const bare =
39
+ scopeIsAll(policy.principal) &&
40
+ scopeIsAll(policy.action) &&
41
+ scopeIsAll(policy.resource) &&
42
+ conditionsOf(policy).length === 0;
43
+ if (!bare) continue;
44
+
45
+ diagnostics.push({
46
+ checkId: "CEDS010",
47
+ severity,
48
+ message: `Policy "${entry.key}" is a bare permit(principal, action, resource) — every principal may take every action on every resource.${prod ? "" : " This is an error in a production build."} Constrain a scope or add a when/unless guard.`,
49
+ entity: entry.key,
50
+ lexicon: set.lexicon,
51
+ });
52
+ }
53
+ }
54
+
55
+ return diagnostics;
56
+ },
57
+ };
@@ -0,0 +1,42 @@
1
+ /**
2
+ * CEDS011: a policy set with no `forbid` has no floor
3
+ *
4
+ * Cedar's evaluation order is fixed and is the reason `forbid` exists: a
5
+ * `forbid` that matches beats every `permit`, no matter how the permits are
6
+ * written or in what order. That makes a `forbid` the only construct in the
7
+ * language that states an invariant — "never outside the corporate network",
8
+ * "never on a resource marked frozen" — which no later grant can widen.
9
+ *
10
+ * A set built only from permits has none. Every future policy is additive, and
11
+ * the blast radius of a careless one is bounded by nothing. This is a
12
+ * convention rather than a bug, so it warns; the fix is one `forbid` carrying
13
+ * whatever the set's real invariant is.
14
+ *
15
+ * Reported once per policy set, not once per permit.
16
+ */
17
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
18
+ import { effectOf, parsedPolicySets } from "./cedar-helpers";
19
+
20
+ export const ceds011: PostSynthCheck = {
21
+ id: "CEDS011",
22
+ description: "A policy set built entirely from permits carries no forbid to bound them",
23
+
24
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
25
+ const diagnostics: PostSynthDiagnostic[] = [];
26
+
27
+ for (const set of parsedPolicySets(ctx)) {
28
+ if (set.entries.length === 0) continue;
29
+ if (set.entries.some((entry) => effectOf(entry.policy) === "forbid")) continue;
30
+
31
+ diagnostics.push({
32
+ checkId: "CEDS011",
33
+ severity: "warning",
34
+ message: `Cedar policy set "${set.source}" has ${set.entries.length} policy/policies and no forbid. A forbid beats every permit regardless of order, so it is the only way to state an invariant a later grant cannot widen — add one for whatever this set must never allow.`,
35
+ entity: set.source,
36
+ lexicon: set.lexicon,
37
+ });
38
+ }
39
+
40
+ return diagnostics;
41
+ },
42
+ };
@@ -0,0 +1,50 @@
1
+ /**
2
+ * CEDS012: a `permit` over every action is IAM's `"Action": "*"`
3
+ *
4
+ * A permit that names a principal or a resource but leaves `action`
5
+ * unconstrained grants every action Cedar's schema defines on that target —
6
+ * including the ones added next quarter, which is what makes it different from
7
+ * a wide-but-enumerated grant. The author almost always meant a specific verb
8
+ * or a group (`action in [Action::"read", Action::"list"]`).
9
+ *
10
+ * Scoped deliberately narrow so it does not double-report: a policy with *no*
11
+ * constraint anywhere and no guard is a bare permit, which is CEDS010's
12
+ * finding and a more serious one. This check speaks only to a policy that
13
+ * constrained something and then left the verb open.
14
+ */
15
+ import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
16
+ import { conditionsOf, effectOf, parsedPolicySets, scopeIsAll } from "./cedar-helpers";
17
+
18
+ export const ceds012: PostSynthCheck = {
19
+ id: "CEDS012",
20
+ description: "A permit that leaves the action scope unconstrained grants every action, including future ones",
21
+
22
+ check(ctx: PostSynthContext): PostSynthDiagnostic[] {
23
+ const diagnostics: PostSynthDiagnostic[] = [];
24
+
25
+ for (const set of parsedPolicySets(ctx)) {
26
+ for (const entry of set.entries) {
27
+ const policy = entry.policy;
28
+ if (effectOf(policy) !== "permit") continue;
29
+ if (!scopeIsAll(policy.action)) continue;
30
+
31
+ // The everything-permit belongs to CEDS010.
32
+ const narrowedElsewhere =
33
+ !scopeIsAll(policy.principal) ||
34
+ !scopeIsAll(policy.resource) ||
35
+ conditionsOf(policy).length > 0;
36
+ if (!narrowedElsewhere) continue;
37
+
38
+ diagnostics.push({
39
+ checkId: "CEDS012",
40
+ severity: "warning",
41
+ message: `Policy "${entry.key}" permits every action — the action scope is unconstrained, so the grant widens on its own as the schema gains actions. Name the actions it needs (action == Action::"read", or action in [ … ]).`,
42
+ entity: entry.key,
43
+ lexicon: set.lexicon,
44
+ });
45
+ }
46
+ }
47
+
48
+ return diagnostics;
49
+ },
50
+ };
@@ -0,0 +1,148 @@
1
+ /**
2
+ * CEDC001: a Cedar policy declaration must have a usable effect and no empty guard
3
+ *
4
+ * Two authoring mistakes that the serializer cannot report and the emitted
5
+ * artifact hides:
6
+ *
7
+ * - **An effect that is not `permit` or `forbid`.** Cedar has exactly two, and
8
+ * the serializer coerces anything that is not the literal string `"forbid"`
9
+ * into `permit`. So `effect: "deny"` — the word every other policy language
10
+ * in the world uses, and the one a reader will reach for — silently emits a
11
+ * *grant*. There is no downstream stage that can catch this: by the time the
12
+ * policy set exists, the mistake looks like a deliberate permit.
13
+ * - **An empty `when`/`unless` entry.** `when: ["", "context.mfa"]` emits
14
+ * `when { }`, which is not a Cedar expression; the policy set then fails to
15
+ * parse and the error arrives as a byte offset into a generated file. The
16
+ * typo is here, in the source, and so is the line number worth printing.
17
+ *
18
+ * Source-level because both are visible only before serialization, and both
19
+ * are about a literal the author typed. A guard built from a variable or a
20
+ * template expression is out of reach of an AST rule and is left alone —
21
+ * CEDC011 covers the emitted form.
22
+ *
23
+ * Scoped by import: only calls to bindings imported from this lexicon are
24
+ * considered, so an unrelated `{ effect: "deny" }` in a file that has nothing
25
+ * to do with Cedar is never flagged.
26
+ */
27
+ import type { LintRule, LintDiagnostic, LintContext } from "@intentius/chant/lint/rule";
28
+ import * as ts from "typescript";
29
+
30
+ /** Module specifiers whose exports construct Cedar declarables. */
31
+ const CEDAR_MODULE = /chant-lexicon-cedar/;
32
+
33
+ /** The two effects Cedar has. */
34
+ export const CEDAR_EFFECTS = ["permit", "forbid"] as const;
35
+
36
+ function importedCedarBindings(sourceFile: ts.SourceFile): Set<string> {
37
+ const names = new Set<string>();
38
+ for (const statement of sourceFile.statements) {
39
+ if (!ts.isImportDeclaration(statement)) continue;
40
+ if (!ts.isStringLiteral(statement.moduleSpecifier)) continue;
41
+ if (!CEDAR_MODULE.test(statement.moduleSpecifier.text)) continue;
42
+
43
+ const bindings = statement.importClause?.namedBindings;
44
+ if (bindings && ts.isNamedImports(bindings)) {
45
+ for (const element of bindings.elements) names.add(element.name.text);
46
+ }
47
+ if (statement.importClause?.name) names.add(statement.importClause.name.text);
48
+ }
49
+ return names;
50
+ }
51
+
52
+ /** The callee identifier of a `new X(…)` / `X(…)` expression, if it has one. */
53
+ function calleeName(node: ts.CallExpression | ts.NewExpression): string | undefined {
54
+ return ts.isIdentifier(node.expression) ? node.expression.text : undefined;
55
+ }
56
+
57
+ /** The last object-literal argument — where chant resources carry their props. */
58
+ function propsLiteral(node: ts.CallExpression | ts.NewExpression): ts.ObjectLiteralExpression | undefined {
59
+ const args = [...(node.arguments ?? [])];
60
+ for (let i = args.length - 1; i >= 0; i--) {
61
+ const arg = args[i];
62
+ if (ts.isObjectLiteralExpression(arg)) return arg;
63
+ }
64
+ return undefined;
65
+ }
66
+
67
+ function property(obj: ts.ObjectLiteralExpression, key: string): ts.PropertyAssignment | undefined {
68
+ for (const prop of obj.properties) {
69
+ if (!ts.isPropertyAssignment(prop)) continue;
70
+ if (!ts.isIdentifier(prop.name) && !ts.isStringLiteral(prop.name)) continue;
71
+ if (prop.name.text === key) return prop;
72
+ }
73
+ return undefined;
74
+ }
75
+
76
+ function lineCol(sourceFile: ts.SourceFile, node: ts.Node): { line: number; column: number } {
77
+ const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
78
+ return { line: line + 1, column: character + 1 };
79
+ }
80
+
81
+ export const cedarPolicyShapeRule: LintRule = {
82
+ id: "CEDC001",
83
+ severity: "error",
84
+ category: "correctness",
85
+ description:
86
+ "A Cedar policy declaration must use effect permit or forbid, and must not carry an empty when/unless clause",
87
+
88
+ check(context: LintContext): LintDiagnostic[] {
89
+ const { sourceFile } = context;
90
+ const diagnostics: LintDiagnostic[] = [];
91
+ const cedarBindings = importedCedarBindings(sourceFile);
92
+ if (cedarBindings.size === 0) return diagnostics;
93
+
94
+ const report = (node: ts.Node, message: string): void => {
95
+ const { line, column } = lineCol(sourceFile, node);
96
+ diagnostics.push({
97
+ file: sourceFile.fileName,
98
+ line,
99
+ column,
100
+ ruleId: "CEDC001",
101
+ severity: "error",
102
+ message,
103
+ });
104
+ };
105
+
106
+ function inspect(node: ts.CallExpression | ts.NewExpression): void {
107
+ const callee = calleeName(node);
108
+ if (!callee || !cedarBindings.has(callee)) return;
109
+ const props = propsLiteral(node);
110
+ if (!props) return;
111
+
112
+ const effect = property(props, "effect");
113
+ if (effect && ts.isStringLiteral(effect.initializer)) {
114
+ const value = effect.initializer.text;
115
+ if (!(CEDAR_EFFECTS as readonly string[]).includes(value)) {
116
+ report(
117
+ effect,
118
+ `Cedar policy effect "${value}" is not a Cedar effect — the language has only permit and forbid, and anything that is not "forbid" is emitted as a permit. Write effect: "forbid" if that is what was meant.`,
119
+ );
120
+ }
121
+ }
122
+
123
+ for (const clause of ["when", "unless"] as const) {
124
+ const guard = property(props, clause);
125
+ if (!guard) continue;
126
+ const entries = ts.isArrayLiteralExpression(guard.initializer)
127
+ ? guard.initializer.elements
128
+ : [guard.initializer];
129
+ for (const entry of entries) {
130
+ if (!ts.isStringLiteral(entry) && !ts.isNoSubstitutionTemplateLiteral(entry)) continue;
131
+ if (entry.text.trim() !== "") continue;
132
+ report(
133
+ entry,
134
+ `Cedar policy has an empty ${clause} clause. An empty guard serializes to ${clause} { }, which is not a Cedar expression and makes the whole policy set unparseable — write the condition, or drop the entry.`,
135
+ );
136
+ }
137
+ }
138
+ }
139
+
140
+ function visit(node: ts.Node): void {
141
+ if (ts.isCallExpression(node) || ts.isNewExpression(node)) inspect(node);
142
+ ts.forEachChild(node, visit);
143
+ }
144
+
145
+ visit(sourceFile);
146
+ return diagnostics;
147
+ },
148
+ };
@@ -0,0 +1,315 @@
1
+ /**
2
+ * The `cedar-wasm` seam for the cedar post-synth checks.
3
+ *
4
+ * Cedar's real parser and validator ship as `@cedar-policy/cedar-wasm`, which
5
+ * runs in-process — no CLI on PATH, no Docker. Everything the checks need from
6
+ * it goes through this module so the traps live in one place. The traps, all
7
+ * of them empirically verified against 4.12.0 (chant #1648):
8
+ *
9
+ * 1. **`type: "success"` does not mean valid.** `failure` means the *call*
10
+ * failed; `success` means validation *ran*, and can carry any number of
11
+ * `validationErrors`. {@link validatePolicySet} asserts both.
12
+ * 2. **`validationErrors` ordering is non-deterministic** — the validator
13
+ * parallelizes across policies and never re-sorts. Forty identical calls
14
+ * produced six distinct orderings. Everything this module returns is sorted
15
+ * by `policyId` then `message`, so a check's output is stable.
16
+ * 3. **Malformed input throws instead of returning a failure answer.** Missing
17
+ * fields on a call struct raise a bare serde `Error` whose `line 1 column N`
18
+ * refers to the internal call struct, not to anything the user wrote — it is
19
+ * meaningless to surface. {@link call} wraps every entry point and normalizes
20
+ * both channels into one result type.
21
+ * 4. **A bare-string policy set gets synthesized ids** (`policy0`, `policy1`),
22
+ * and `@id` annotations do *not* become the policy id. Everything here
23
+ * passes `staticPolicies` as a `Record<PolicyId, Policy>` keyed by chant's
24
+ * own ids, so a diagnostic names something a reader can find.
25
+ * 5. **Only `strict` validation mode exists** in this build; any other value
26
+ * throws. The settings are left at their default rather than passed.
27
+ *
28
+ * Only the `/nodejs` flavor is imported. The root/`esm` flavor imports the
29
+ * `.wasm` as an ES module, which Bun cannot start (`__wbindgen_start is not a
30
+ * function`) and Node only supports behind an experimental warning printed on
31
+ * every run; `/web` needs an explicit `initSync`. `/nodejs` is the one flavor
32
+ * that loads under `node`, `tsx`, `vitest`, and `bun` alike.
33
+ *
34
+ * Excluded from check auto-discovery by the "helper" filename filter.
35
+ */
36
+ import { createRequire } from "module";
37
+ import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
38
+ import type * as CedarWasm from "@cedar-policy/cedar-wasm/nodejs";
39
+ import {
40
+ isRecord,
41
+ type CedarPolicyJson,
42
+ type CedarPolicySet,
43
+ type CedarPolicySetDoc,
44
+ } from "./cedar-helpers";
45
+
46
+ // ── Loading ───────────────────────────────────────────────────────
47
+
48
+ type Wasm = typeof CedarWasm;
49
+
50
+ let cached: Wasm | undefined;
51
+ let loadError: string | undefined;
52
+
53
+ /**
54
+ * The wasm module, loaded on first use.
55
+ *
56
+ * Lazy and `require`-based on purpose. The post-synth barrel is imported
57
+ * statically by `plugin.ts`, and every `chant lint`/`chant audit` run loads
58
+ * every active lexicon's plugin — a top-level import would pull 4 MB of wasm
59
+ * into any build that merely has cedar installed. `check()` is synchronous, so
60
+ * `await import()` is not available; `createRequire` is. The `/nodejs` flavor
61
+ * is CommonJS with a synchronous `readFileSync` init, so there is nothing to
62
+ * await anyway.
63
+ */
64
+ export function loadWasm(): Wasm | undefined {
65
+ if (cached || loadError) return cached;
66
+ try {
67
+ const require = createRequire(import.meta.url);
68
+ cached = require("@cedar-policy/cedar-wasm/nodejs") as Wasm;
69
+ } catch (err) {
70
+ loadError = err instanceof Error ? err.message : String(err);
71
+ }
72
+ return cached;
73
+ }
74
+
75
+ /** Why the wasm could not be loaded, if it could not be. */
76
+ export function wasmLoadError(): string | undefined {
77
+ loadWasm();
78
+ return loadError;
79
+ }
80
+
81
+ /** Reset the module cache. Test-only — lets a test exercise the load-failure path. */
82
+ export function resetWasmCache(): void {
83
+ cached = undefined;
84
+ loadError = undefined;
85
+ }
86
+
87
+ // ── Calling ───────────────────────────────────────────────────────
88
+
89
+ export type WasmResult<T> = { ok: true; value: T } | { ok: false; message: string };
90
+
91
+ /**
92
+ * Run a wasm call, turning a thrown serde error into a result rather than an
93
+ * exception (trap 3 above). The thrown message is deliberately not passed
94
+ * through verbatim: its positions point into the internal call struct.
95
+ */
96
+ export function call<T>(what: string, fn: () => T): WasmResult<T> {
97
+ try {
98
+ return { ok: true, value: fn() };
99
+ } catch (err) {
100
+ const raw = err instanceof Error ? err.message : String(err);
101
+ // Strip the internal `at line 1 column 42` suffix — it locates a field in
102
+ // the serialized call struct, never anything in the user's policy.
103
+ const message = raw.replace(/\s+at line \d+ column \d+\.?$/, "");
104
+ return { ok: false, message: `${what} could not be run by cedar-wasm: ${message}` };
105
+ }
106
+ }
107
+
108
+ /** The first line of a Cedar `DetailedError`, with its `help` when it has one. */
109
+ export function describeError(error: { message: string; help?: string | null }): string {
110
+ return error.help ? `${error.message} (${error.help})` : error.message;
111
+ }
112
+
113
+ // ── Condition bodies ──────────────────────────────────────────────
114
+
115
+ /**
116
+ * chant's serializer carries a `when`/`unless` body as Cedar expression
117
+ * *source* under a `__expr` key, because the authored model holds expression
118
+ * strings until schema-driven codegen (#1650) makes them typed trees. `__expr`
119
+ * is not part of Cedar's JSON policy grammar — the deserializer rejects it
120
+ * ("unknown variant `__expr`") — so anything handed to the wasm has to have
121
+ * those bodies parsed into real expression JSON first.
122
+ *
123
+ * There is no expression-level entry point in the package, so the expression
124
+ * is parsed inside a throwaway policy and the resulting condition body lifted
125
+ * back out. A source expression that does not parse is the finding CEDC011
126
+ * reports.
127
+ */
128
+ export const EXPR_KEY = "__expr";
129
+
130
+ export interface ExpressionFailure {
131
+ /** The policy key the clause belongs to. */
132
+ key: string;
133
+ /** `when` or `unless`, as the document spelled it. */
134
+ kind: string;
135
+ /** The expression source that failed. */
136
+ expression: string;
137
+ message: string;
138
+ }
139
+
140
+ export interface NormalizedPolicySet {
141
+ /** A policy set the wasm will accept, keyed by chant's own policy ids. */
142
+ policySet: CedarWasm.PolicySet;
143
+ /** Condition bodies whose expression source did not parse. */
144
+ expressionFailures: ExpressionFailure[];
145
+ }
146
+
147
+ function parseExpression(wasm: Wasm, expression: string): WasmResult<unknown> {
148
+ const answer = call("condition expression", () =>
149
+ wasm.policyToJson(`permit(principal, action, resource) when { ${expression} };`),
150
+ );
151
+ if (!answer.ok) return answer;
152
+ if (answer.value.type === "failure") {
153
+ return { ok: false, message: answer.value.errors.map(describeError).join("; ") };
154
+ }
155
+ const conditions = answer.value.json.conditions;
156
+ const body = Array.isArray(conditions) && isRecord(conditions[0]) ? conditions[0].body : undefined;
157
+ if (body === undefined) return { ok: false, message: "expression parsed to no condition body" };
158
+ return { ok: true, value: body };
159
+ }
160
+
161
+ /** Replace every `__expr` body in a policy set with parsed expression JSON. */
162
+ export function normalizePolicySet(wasm: Wasm, set: CedarPolicySet): NormalizedPolicySet {
163
+ const staticPolicies: Record<string, unknown> = {};
164
+ const templates: Record<string, unknown> = {};
165
+ const expressionFailures: ExpressionFailure[] = [];
166
+
167
+ for (const entry of set.entries) {
168
+ const target = entry.template ? templates : staticPolicies;
169
+ target[entry.key] = normalizePolicy(wasm, entry.key, entry.policy, expressionFailures);
170
+ }
171
+
172
+ const doc: CedarPolicySetDoc = set.doc ?? {};
173
+ const policySet = {
174
+ staticPolicies,
175
+ templates,
176
+ templateLinks: Array.isArray(doc.templateLinks) ? doc.templateLinks : [],
177
+ } as unknown as CedarWasm.PolicySet;
178
+
179
+ return { policySet, expressionFailures };
180
+ }
181
+
182
+ function normalizePolicy(
183
+ wasm: Wasm,
184
+ key: string,
185
+ policy: CedarPolicyJson,
186
+ failures: ExpressionFailure[],
187
+ ): CedarPolicyJson {
188
+ if (!Array.isArray(policy.conditions)) return policy;
189
+
190
+ const conditions = policy.conditions.map((clause) => {
191
+ if (!isRecord(clause) || !isRecord(clause.body)) return clause;
192
+ const expression = clause.body[EXPR_KEY];
193
+ if (typeof expression !== "string") return clause;
194
+
195
+ const parsed = parseExpression(wasm, expression);
196
+ if (!parsed.ok) {
197
+ failures.push({
198
+ key,
199
+ kind: typeof clause.kind === "string" ? clause.kind : "when",
200
+ expression,
201
+ message: parsed.message,
202
+ });
203
+ // Leave a body the deserializer accepts so the rest of the set still
204
+ // validates — the unparseable clause is reported by CEDC011, not by a
205
+ // cascade of confusing structural errors from every other check.
206
+ return { ...clause, body: { Value: true } };
207
+ }
208
+ return { ...clause, body: parsed.value };
209
+ });
210
+
211
+ return { ...policy, conditions };
212
+ }
213
+
214
+ // ── Schema discovery ──────────────────────────────────────────────
215
+
216
+ /** A Cedar schema found beside the policies in the build output. */
217
+ export interface DiscoveredSchema {
218
+ /** Human-readable `.cedarschema` source, or a parsed JSON schema object. */
219
+ schema: CedarWasm.Schema;
220
+ /** The filename it came from. */
221
+ source: string;
222
+ }
223
+
224
+ /**
225
+ * The project's Cedar schema, if the build emitted one beside the policies.
226
+ *
227
+ * Two forms, both of which the wasm accepts — but only as the right JS type: a
228
+ * `string` is *always* read as human-readable `.cedarschema`, never as JSON, so
229
+ * a `.cedarschema.json` file has to be `JSON.parse`d into a live object first
230
+ * or the parser reports "unexpected token `{`" against perfectly valid JSON.
231
+ *
232
+ * Reads `ctx.outputs` only. Nothing emits a schema today — schema-driven
233
+ * codegen is #1650 — which is exactly why CEDE010 says so out loud instead of
234
+ * passing silently.
235
+ */
236
+ export function findSchema(ctx: PostSynthContext): DiscoveredSchema | undefined {
237
+ for (const [, output] of ctx.outputs) {
238
+ if (typeof output === "string") continue;
239
+ for (const [filename, content] of Object.entries(output.files ?? {})) {
240
+ if (typeof content !== "string") continue;
241
+ if (filename.endsWith(".cedarschema.json")) {
242
+ try {
243
+ const parsed: unknown = JSON.parse(content);
244
+ if (isRecord(parsed)) return { schema: parsed as CedarWasm.Schema, source: filename };
245
+ } catch {
246
+ continue; // a schema that is not JSON is not a schema we can use
247
+ }
248
+ } else if (filename.endsWith(".cedarschema")) {
249
+ return { schema: content, source: filename };
250
+ }
251
+ }
252
+ }
253
+ return undefined;
254
+ }
255
+
256
+ // ── Validation ────────────────────────────────────────────────────
257
+
258
+ /** One validator finding, already flattened and safe to sort. */
259
+ export interface ValidationFinding {
260
+ policyId: string;
261
+ message: string;
262
+ }
263
+
264
+ export interface ValidationOutcome {
265
+ /** Sorted by `policyId` then `message` — the wasm's own order is not stable. */
266
+ errors: ValidationFinding[];
267
+ /** Same sort. */
268
+ warnings: ValidationFinding[];
269
+ /** Set when the call itself failed (a parse error, a malformed call struct). */
270
+ failure?: string;
271
+ }
272
+
273
+ function sortFindings(findings: ValidationFinding[]): ValidationFinding[] {
274
+ return [...findings].sort(
275
+ (a, b) => a.policyId.localeCompare(b.policyId) || a.message.localeCompare(b.message),
276
+ );
277
+ }
278
+
279
+ function flatten(errors: CedarWasm.ValidationError[]): ValidationFinding[] {
280
+ return errors.map((e) => ({ policyId: e.policyId, message: describeError(e.error) }));
281
+ }
282
+
283
+ /** Validate a normalized policy set against a schema. Never throws. */
284
+ export function validatePolicySet(
285
+ wasm: Wasm,
286
+ policySet: CedarWasm.PolicySet,
287
+ schema: CedarWasm.Schema,
288
+ ): ValidationOutcome {
289
+ const answer = call("validate", () => wasm.validate({ schema, policies: policySet }));
290
+ if (!answer.ok) return { errors: [], warnings: [], failure: answer.message };
291
+
292
+ if (answer.value.type === "failure") {
293
+ return {
294
+ errors: [],
295
+ warnings: [],
296
+ failure: answer.value.errors.map(describeError).join("; "),
297
+ };
298
+ }
299
+
300
+ // `success` only means validation RAN. The findings are in the arrays.
301
+ return {
302
+ errors: sortFindings(flatten(answer.value.validationErrors)),
303
+ warnings: sortFindings(flatten(answer.value.validationWarnings)),
304
+ };
305
+ }
306
+
307
+ /** Parse-only gate, for when there is no schema to validate against. */
308
+ export function parsePolicySet(wasm: Wasm, policySet: CedarWasm.PolicySet): WasmResult<string[]> {
309
+ const answer = call("checkParsePolicySet", () => wasm.checkParsePolicySet(policySet));
310
+ if (!answer.ok) return answer;
311
+ if (answer.value.type === "failure") {
312
+ return { ok: true, value: answer.value.errors.map(describeError).sort() };
313
+ }
314
+ return { ok: true, value: [] };
315
+ }