@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.
- package/README.md +190 -0
- package/dist/avp/ambient.d.ts +54 -0
- package/dist/avp/ambient.d.ts.map +1 -0
- package/dist/avp/client.d.ts +127 -0
- package/dist/avp/client.d.ts.map +1 -0
- package/dist/avp/describe-resources.d.ts +42 -0
- package/dist/avp/describe-resources.d.ts.map +1 -0
- package/dist/avp/embed.d.ts +120 -0
- package/dist/avp/embed.d.ts.map +1 -0
- package/dist/avp/live-export.d.ts +94 -0
- package/dist/avp/live-export.d.ts.map +1 -0
- package/dist/avp/ownership.d.ts +97 -0
- package/dist/avp/ownership.d.ts.map +1 -0
- package/dist/avp/statement.d.ts +22 -0
- package/dist/avp/statement.d.ts.map +1 -0
- package/dist/avp/store.d.ts +82 -0
- package/dist/avp/store.d.ts.map +1 -0
- package/dist/avp/testdata/mock-transport.d.ts +55 -0
- package/dist/avp/testdata/mock-transport.d.ts.map +1 -0
- package/dist/codegen/docs-cli.d.ts +3 -0
- package/dist/codegen/docs-cli.d.ts.map +1 -0
- package/dist/codegen/docs.d.ts +26 -0
- package/dist/codegen/docs.d.ts.map +1 -0
- package/dist/codegen/emit.d.ts +85 -0
- package/dist/codegen/emit.d.ts.map +1 -0
- package/dist/codegen/generate-cli.d.ts +3 -0
- package/dist/codegen/generate-cli.d.ts.map +1 -0
- package/dist/codegen/generate.d.ts +37 -0
- package/dist/codegen/generate.d.ts.map +1 -0
- package/dist/codegen/naming.d.ts +48 -0
- package/dist/codegen/naming.d.ts.map +1 -0
- package/dist/codegen/package.d.ts +10 -0
- package/dist/codegen/package.d.ts.map +1 -0
- package/dist/composites/deny-by-default-set.d.ts +58 -0
- package/dist/composites/deny-by-default-set.d.ts.map +1 -0
- package/dist/composites/index.d.ts +12 -0
- package/dist/composites/index.d.ts.map +1 -0
- package/dist/composites/owner-can-manage.d.ts +48 -0
- package/dist/composites/owner-can-manage.d.ts.map +1 -0
- package/dist/config.d.ts +102 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/coverage.d.ts +59 -0
- package/dist/coverage.d.ts.map +1 -0
- package/dist/detect.d.ts +18 -0
- package/dist/detect.d.ts.map +1 -0
- package/dist/generated/index.d.ts +251 -0
- package/dist/generated/index.d.ts.map +1 -0
- package/dist/generated/runtime.d.ts +2 -0
- package/dist/generated/runtime.d.ts.map +1 -0
- package/dist/import/adapter.d.ts +21 -0
- package/dist/import/adapter.d.ts.map +1 -0
- package/dist/import/clause-text.d.ts +38 -0
- package/dist/import/clause-text.d.ts.map +1 -0
- package/dist/import/generator.d.ts +53 -0
- package/dist/import/generator.d.ts.map +1 -0
- package/dist/import/parser.d.ts +57 -0
- package/dist/import/parser.d.ts.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/init-templates.d.ts +32 -0
- package/dist/init-templates.d.ts.map +1 -0
- package/dist/integrity.json +22 -0
- package/dist/lint/audit-catalog.d.ts +29 -0
- package/dist/lint/audit-catalog.d.ts.map +1 -0
- package/dist/lint/post-synth/cedar-helpers.d.ts +97 -0
- package/dist/lint/post-synth/cedar-helpers.d.ts.map +1 -0
- package/dist/lint/post-synth/cedc010.d.ts +17 -0
- package/dist/lint/post-synth/cedc010.d.ts.map +1 -0
- package/dist/lint/post-synth/cedc011.d.ts +17 -0
- package/dist/lint/post-synth/cedc011.d.ts.map +1 -0
- package/dist/lint/post-synth/cedc012.d.ts +21 -0
- package/dist/lint/post-synth/cedc012.d.ts.map +1 -0
- package/dist/lint/post-synth/cedc013.d.ts +20 -0
- package/dist/lint/post-synth/cedc013.d.ts.map +1 -0
- package/dist/lint/post-synth/cedc014.d.ts +19 -0
- package/dist/lint/post-synth/cedc014.d.ts.map +1 -0
- package/dist/lint/post-synth/cede010.d.ts +34 -0
- package/dist/lint/post-synth/cede010.d.ts.map +1 -0
- package/dist/lint/post-synth/cede011.d.ts +22 -0
- package/dist/lint/post-synth/cede011.d.ts.map +1 -0
- package/dist/lint/post-synth/ceds010.d.ts +24 -0
- package/dist/lint/post-synth/ceds010.d.ts.map +1 -0
- package/dist/lint/post-synth/ceds011.d.ts +19 -0
- package/dist/lint/post-synth/ceds011.d.ts.map +1 -0
- package/dist/lint/post-synth/ceds012.d.ts +17 -0
- package/dist/lint/post-synth/ceds012.d.ts.map +1 -0
- package/dist/lint/post-synth/index.d.ts +3 -0
- package/dist/lint/post-synth/index.d.ts.map +1 -0
- package/dist/lint/post-synth/wasm-helpers.d.ts +108 -0
- package/dist/lint/post-synth/wasm-helpers.d.ts.map +1 -0
- package/dist/lint/rules/index.d.ts +10 -0
- package/dist/lint/rules/index.d.ts.map +1 -0
- package/dist/lint/rules/policy-shape.d.ts +31 -0
- package/dist/lint/rules/policy-shape.d.ts.map +1 -0
- package/dist/lsp/completions.d.ts +24 -0
- package/dist/lsp/completions.d.ts.map +1 -0
- package/dist/lsp/hover.d.ts +11 -0
- package/dist/lsp/hover.d.ts.map +1 -0
- package/dist/lsp/registry.d.ts +45 -0
- package/dist/lsp/registry.d.ts.map +1 -0
- package/dist/manifest.json +8 -0
- package/dist/mcp/index.d.ts +34 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/policy-coverage.d.ts +81 -0
- package/dist/mcp/policy-coverage.d.ts.map +1 -0
- package/dist/meta.json +699 -0
- package/dist/okf/index.md +37 -0
- package/dist/okf/rules/CEDC001.md +11 -0
- package/dist/okf/rules/CEDC010.md +11 -0
- package/dist/okf/rules/CEDC011.md +15 -0
- package/dist/okf/rules/CEDC012.md +11 -0
- package/dist/okf/rules/CEDC013.md +15 -0
- package/dist/okf/rules/CEDC014.md +15 -0
- package/dist/okf/rules/CEDE010.md +15 -0
- package/dist/okf/rules/CEDE011.md +15 -0
- package/dist/okf/rules/CEDS010.md +15 -0
- package/dist/okf/rules/CEDS011.md +11 -0
- package/dist/okf/rules/CEDS012.md +15 -0
- package/dist/okf/types/AdminAction.md +14 -0
- package/dist/okf/types/Application.md +14 -0
- package/dist/okf/types/ApproveAction.md +13 -0
- package/dist/okf/types/CommentAction.md +14 -0
- package/dist/okf/types/CreateAction.md +14 -0
- package/dist/okf/types/DeleteAction.md +14 -0
- package/dist/okf/types/Document.md +18 -0
- package/dist/okf/types/Folder.md +15 -0
- package/dist/okf/types/Group.md +14 -0
- package/dist/okf/types/ListAction.md +14 -0
- package/dist/okf/types/Policy.md +29 -0
- package/dist/okf/types/ReadAction.md +14 -0
- package/dist/okf/types/ServiceAccount.md +15 -0
- package/dist/okf/types/ShareAction.md +14 -0
- package/dist/okf/types/Team.md +14 -0
- package/dist/okf/types/User.md +18 -0
- package/dist/okf/types/WriteAction.md +14 -0
- package/dist/package-cli.d.ts +3 -0
- package/dist/package-cli.d.ts.map +1 -0
- package/dist/plugin.d.ts +8 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/rules/cedar-helpers.ts +209 -0
- package/dist/rules/cedc010.ts +80 -0
- package/dist/rules/cedc011.ts +47 -0
- package/dist/rules/cedc012.ts +64 -0
- package/dist/rules/cedc013.ts +68 -0
- package/dist/rules/cedc014.ts +73 -0
- package/dist/rules/cede010.ts +89 -0
- package/dist/rules/cede011.ts +58 -0
- package/dist/rules/ceds010.ts +57 -0
- package/dist/rules/ceds011.ts +42 -0
- package/dist/rules/ceds012.ts +50 -0
- package/dist/rules/policy-shape.ts +148 -0
- package/dist/rules/wasm-helpers.ts +315 -0
- package/dist/serializer.d.ts +138 -0
- package/dist/serializer.d.ts.map +1 -0
- package/dist/spec/fetch.d.ts +71 -0
- package/dist/spec/fetch.d.ts.map +1 -0
- package/dist/spec/parse.d.ts +113 -0
- package/dist/spec/parse.d.ts.map +1 -0
- package/dist/spec/pin.d.ts +116 -0
- package/dist/spec/pin.d.ts.map +1 -0
- package/dist/spec/pinned-names.json +18 -0
- package/dist/spec/wasm.d.ts +143 -0
- package/dist/spec/wasm.d.ts.map +1 -0
- package/dist/types/index.d.ts +219 -0
- package/dist/validate-cli.d.ts +3 -0
- package/dist/validate-cli.d.ts.map +1 -0
- package/dist/validate.d.ts +25 -0
- package/dist/validate.d.ts.map +1 -0
- package/package.json +75 -0
- package/src/avp/OWNERSHIP.md +125 -0
- package/src/avp/ambient.test.ts +113 -0
- package/src/avp/ambient.ts +124 -0
- package/src/avp/client.ts +310 -0
- package/src/avp/describe-resources.test.ts +316 -0
- package/src/avp/describe-resources.ts +215 -0
- package/src/avp/embed.test.ts +114 -0
- package/src/avp/embed.ts +190 -0
- package/src/avp/live-export.test.ts +232 -0
- package/src/avp/live-export.ts +185 -0
- package/src/avp/ownership.test.ts +101 -0
- package/src/avp/ownership.ts +170 -0
- package/src/avp/statement.ts +50 -0
- package/src/avp/store.ts +152 -0
- package/src/avp/testdata/mock-transport.ts +147 -0
- package/src/codegen/docs-cli.ts +7 -0
- package/src/codegen/docs.ts +873 -0
- package/src/codegen/emit.ts +496 -0
- package/src/codegen/generate-cli.ts +18 -0
- package/src/codegen/generate.test.ts +128 -0
- package/src/codegen/generate.ts +123 -0
- package/src/codegen/naming.ts +101 -0
- package/src/codegen/package.ts +52 -0
- package/src/composites/composites.test.ts +206 -0
- package/src/composites/deny-by-default-set.ts +98 -0
- package/src/composites/index.ts +13 -0
- package/src/composites/owner-can-manage.ts +80 -0
- package/src/config-namespace.test.ts +44 -0
- package/src/config.test.ts +82 -0
- package/src/config.ts +119 -0
- package/src/coverage.test.ts +61 -0
- package/src/coverage.ts +166 -0
- package/src/detect.test.ts +64 -0
- package/src/detect.ts +59 -0
- package/src/generated/index.d.ts +219 -0
- package/src/generated/index.ts +279 -0
- package/src/generated/lexicon-cedar.json +699 -0
- package/src/generated/runtime.ts +2 -0
- package/src/import/adapter.ts +63 -0
- package/src/import/clause-text.ts +178 -0
- package/src/import/generator.test.ts +133 -0
- package/src/import/generator.ts +219 -0
- package/src/import/parser.test.ts +265 -0
- package/src/import/parser.ts +352 -0
- package/src/import/roundtrip.test.ts +127 -0
- package/src/import/testdata/full.cedar +36 -0
- package/src/import/testdata/full.cedar.json +238 -0
- package/src/import/testdata/realistic.cedar +35 -0
- package/src/import/testdata/simple.cedar +6 -0
- package/src/index.ts +94 -0
- package/src/init-templates.test.ts +158 -0
- package/src/init-templates.ts +358 -0
- package/src/lint/audit-catalog.ts +130 -0
- package/src/lint/post-synth/cedar-helpers.ts +209 -0
- package/src/lint/post-synth/cedc010.ts +80 -0
- package/src/lint/post-synth/cedc011.ts +47 -0
- package/src/lint/post-synth/cedc012.ts +64 -0
- package/src/lint/post-synth/cedc013.ts +68 -0
- package/src/lint/post-synth/cedc014.ts +73 -0
- package/src/lint/post-synth/cede010.ts +89 -0
- package/src/lint/post-synth/cede011.ts +58 -0
- package/src/lint/post-synth/ceds010.ts +57 -0
- package/src/lint/post-synth/ceds011.ts +42 -0
- package/src/lint/post-synth/ceds012.ts +50 -0
- package/src/lint/post-synth/index.ts +25 -0
- package/src/lint/post-synth/post-synth.test.ts +474 -0
- package/src/lint/post-synth/wasm-helpers.ts +315 -0
- package/src/lint/rules/index.ts +12 -0
- package/src/lint/rules/policy-shape.test.ts +90 -0
- package/src/lint/rules/policy-shape.ts +148 -0
- package/src/lsp/completions.test.ts +111 -0
- package/src/lsp/completions.ts +55 -0
- package/src/lsp/hover.test.ts +82 -0
- package/src/lsp/hover.ts +75 -0
- package/src/lsp/registry.ts +74 -0
- package/src/mcp/index.ts +103 -0
- package/src/mcp/policy-coverage.test.ts +176 -0
- package/src/mcp/policy-coverage.ts +205 -0
- package/src/package-cli.ts +22 -0
- package/src/plugin.test.ts +115 -0
- package/src/plugin.ts +312 -0
- package/src/serializer.test.ts +502 -0
- package/src/serializer.ts +335 -0
- package/src/skills/chant-cedar-authoring.md +180 -0
- package/src/skills/chant-cedar-avp-embedding.md +125 -0
- package/src/skills/chant-cedar-meta-policy.md +119 -0
- package/src/spec/default-schema.cedarschema +108 -0
- package/src/spec/fetch.ts +107 -0
- package/src/spec/parse.test.ts +123 -0
- package/src/spec/parse.ts +253 -0
- package/src/spec/pin.test.ts +88 -0
- package/src/spec/pin.ts +243 -0
- package/src/spec/pinned-names.json +18 -0
- package/src/spec/wasm.ts +283 -0
- package/src/validate-cli.ts +8 -0
- package/src/validate.ts +85 -0
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Positive and negative tests for every cedar post-synth check.
|
|
3
|
+
*
|
|
4
|
+
* Contexts are built with `createPostSynthContext` from the shared test utils
|
|
5
|
+
* (the pattern the post-synth authoring guide documents). It serializes each
|
|
6
|
+
* output object to JSON, which is exactly the artifact these checks read — the
|
|
7
|
+
* `policies.cedar.json` policy-set envelope. `makePostSynthCtxFromFiles` covers
|
|
8
|
+
* the two places the multi-file `SerializerResult` shape is load-bearing: the
|
|
9
|
+
* schema discovery CEDE010/CEDE011 need, and the "two policy sets in one
|
|
10
|
+
* build" case CEDC012 exists for.
|
|
11
|
+
*/
|
|
12
|
+
import { describe, test, expect } from "vitest";
|
|
13
|
+
import { createPostSynthContext, makePostSynthCtxFromFiles } from "@intentius/chant-test-utils";
|
|
14
|
+
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
15
|
+
import { CEDAR_JSON_FILENAME } from "../../serializer";
|
|
16
|
+
import { postSynthChecks } from ".";
|
|
17
|
+
import { cedc010 } from "./cedc010";
|
|
18
|
+
import { cedc011 } from "./cedc011";
|
|
19
|
+
import { cedc012 } from "./cedc012";
|
|
20
|
+
import { cedc013 } from "./cedc013";
|
|
21
|
+
import { cedc014 } from "./cedc014";
|
|
22
|
+
import { cede010 } from "./cede010";
|
|
23
|
+
import { cede011 } from "./cede011";
|
|
24
|
+
import { ceds010 } from "./ceds010";
|
|
25
|
+
import { ceds011 } from "./ceds011";
|
|
26
|
+
import { ceds012 } from "./ceds012";
|
|
27
|
+
|
|
28
|
+
// ── Fixtures ───────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
const ALL = { op: "All" } as const;
|
|
31
|
+
|
|
32
|
+
/** A policy in the shape the serializer emits. */
|
|
33
|
+
function policy(over: Record<string, unknown> = {}): Record<string, unknown> {
|
|
34
|
+
return {
|
|
35
|
+
effect: "permit",
|
|
36
|
+
principal: ALL,
|
|
37
|
+
action: ALL,
|
|
38
|
+
resource: ALL,
|
|
39
|
+
conditions: [],
|
|
40
|
+
annotations: {},
|
|
41
|
+
...over,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function uid(type: string, id: string): Record<string, unknown> {
|
|
46
|
+
return { type, id };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** A policy set with the annotations chant's serializer always writes. */
|
|
50
|
+
function set(policies: Record<string, Record<string, unknown>>): Record<string, unknown> {
|
|
51
|
+
const staticPolicies: Record<string, unknown> = {};
|
|
52
|
+
for (const [id, p] of Object.entries(policies)) {
|
|
53
|
+
const annotations = { id, ...(p.annotations as Record<string, unknown> | undefined) };
|
|
54
|
+
staticPolicies[id] = { ...p, annotations };
|
|
55
|
+
}
|
|
56
|
+
return { staticPolicies, templates: {}, templateLinks: [] };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function ctx(policies: Record<string, Record<string, unknown>>, env?: string): PostSynthContext {
|
|
60
|
+
return { ...createPostSynthContext({ cedar: set(policies) }), env };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** A safe, fully-constrained policy set that no check has anything to say about. */
|
|
64
|
+
const CLEAN = {
|
|
65
|
+
"allow-admin-read": policy({
|
|
66
|
+
principal: { op: "in", entity: uid("Chant::Group", "admins") },
|
|
67
|
+
action: { op: "==", entity: uid("Chant::Action", "read") },
|
|
68
|
+
resource: { op: "is", entity_type: "Chant::Repo" },
|
|
69
|
+
conditions: [{ kind: "when", body: { __expr: "context.mfa == true" } }],
|
|
70
|
+
}),
|
|
71
|
+
"forbid-frozen": policy({
|
|
72
|
+
effect: "forbid",
|
|
73
|
+
resource: { op: "is", entity_type: "Chant::Repo" },
|
|
74
|
+
conditions: [{ kind: "when", body: { __expr: "resource.frozen == true" } }],
|
|
75
|
+
}),
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// A schema covering the CLEAN set, for the validator-backed checks.
|
|
79
|
+
const SCHEMA = `
|
|
80
|
+
namespace Chant {
|
|
81
|
+
entity Group;
|
|
82
|
+
entity User in [Group] { frozen?: Bool };
|
|
83
|
+
entity Repo { frozen: Bool };
|
|
84
|
+
action read appliesTo { principal: [User], resource: [Repo], context: { mfa: Bool } };
|
|
85
|
+
action write appliesTo { principal: [User], resource: [Repo], context: { mfa: Bool } };
|
|
86
|
+
}
|
|
87
|
+
`;
|
|
88
|
+
|
|
89
|
+
function ctxWithSchema(
|
|
90
|
+
policies: Record<string, Record<string, unknown>>,
|
|
91
|
+
schema: string = SCHEMA,
|
|
92
|
+
): PostSynthContext {
|
|
93
|
+
return makePostSynthCtxFromFiles(
|
|
94
|
+
"cedar",
|
|
95
|
+
{
|
|
96
|
+
[CEDAR_JSON_FILENAME]: JSON.stringify(set(policies)),
|
|
97
|
+
"chant.cedarschema": schema,
|
|
98
|
+
},
|
|
99
|
+
"// cedar text",
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ── Barrel ─────────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
describe("the cedar post-synth barrel", () => {
|
|
106
|
+
test("ships every check exactly once, all under the CED prefix", () => {
|
|
107
|
+
const ids = postSynthChecks.map((c) => c.id);
|
|
108
|
+
expect(new Set(ids).size).toBe(ids.length);
|
|
109
|
+
expect(ids.every((id) => id.startsWith("CED"))).toBe(true);
|
|
110
|
+
expect(ids.length).toBeGreaterThanOrEqual(10);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("every check is silent on a build with no cedar output", () => {
|
|
114
|
+
const empty = createPostSynthContext({ k8s: { kind: "ConfigMap" } });
|
|
115
|
+
for (const check of postSynthChecks) {
|
|
116
|
+
expect(check.check(empty), check.id).toEqual([]);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("every check is silent on a clean, fully-constrained policy set", () => {
|
|
121
|
+
const clean = ctxWithSchema(CLEAN);
|
|
122
|
+
for (const check of postSynthChecks) {
|
|
123
|
+
expect(check.check(clean).map((d) => d.message), check.id).toEqual([]);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// ── CEDC010 ────────────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
describe("CEDC010: the emitted policy set must parse as Cedar", () => {
|
|
131
|
+
test("flags a policy set that is not readable as JSON", () => {
|
|
132
|
+
const broken: PostSynthContext = {
|
|
133
|
+
...createPostSynthContext({}),
|
|
134
|
+
outputs: new Map([["cedar", "{ not json"]]),
|
|
135
|
+
};
|
|
136
|
+
const diags = cedc010.check(broken);
|
|
137
|
+
expect(diags).toHaveLength(1);
|
|
138
|
+
expect(diags[0].checkId).toBe("CEDC010");
|
|
139
|
+
expect(diags[0].severity).toBe("error");
|
|
140
|
+
expect(diags[0].message).toContain("not readable as JSON");
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("flags a policy whose scope Cedar's deserializer rejects", () => {
|
|
144
|
+
const diags = cedc010.check(ctx({ bad: policy({ principal: { op: "nonsense" } }) }));
|
|
145
|
+
expect(diags.length).toBeGreaterThanOrEqual(1);
|
|
146
|
+
expect(diags[0].checkId).toBe("CEDC010");
|
|
147
|
+
expect(diags[0].severity).toBe("error");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("passes a policy set Cedar parses", () => {
|
|
151
|
+
expect(cedc010.check(ctx(CLEAN))).toHaveLength(0);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// ── CEDC011 ────────────────────────────────────────────────────────
|
|
156
|
+
|
|
157
|
+
describe("CEDC011: condition expressions must parse", () => {
|
|
158
|
+
test("flags an unparseable when clause and names the expression", () => {
|
|
159
|
+
const diags = cedc011.check(
|
|
160
|
+
ctx({ "bad-guard": policy({ conditions: [{ kind: "when", body: { __expr: "context.mfa ==" } }] }) }),
|
|
161
|
+
);
|
|
162
|
+
expect(diags).toHaveLength(1);
|
|
163
|
+
expect(diags[0].checkId).toBe("CEDC011");
|
|
164
|
+
expect(diags[0].severity).toBe("error");
|
|
165
|
+
expect(diags[0].entity).toBe("bad-guard");
|
|
166
|
+
expect(diags[0].message).toContain("context.mfa ==");
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("flags an unparseable unless clause too", () => {
|
|
170
|
+
const diags = cedc011.check(
|
|
171
|
+
ctx({ "bad-guard": policy({ conditions: [{ kind: "unless", body: { __expr: "resource." } }] }) }),
|
|
172
|
+
);
|
|
173
|
+
expect(diags).toHaveLength(1);
|
|
174
|
+
expect(diags[0].message).toContain("unless clause");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("passes expressions Cedar parses", () => {
|
|
178
|
+
expect(cedc011.check(ctx(CLEAN))).toHaveLength(0);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// ── CEDC012 ────────────────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
describe("CEDC012: no two policies may claim the same id", () => {
|
|
185
|
+
test("flags two policies annotating the same @id", () => {
|
|
186
|
+
const clash = {
|
|
187
|
+
staticPolicies: {
|
|
188
|
+
"allow-read": { ...policy(), annotations: { id: "shared" } },
|
|
189
|
+
"allow-list": { ...policy(), annotations: { id: "shared" } },
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
const diags = cedc012.check(createPostSynthContext({ cedar: clash }));
|
|
193
|
+
expect(diags).toHaveLength(1);
|
|
194
|
+
expect(diags[0].checkId).toBe("CEDC012");
|
|
195
|
+
expect(diags[0].severity).toBe("error");
|
|
196
|
+
expect(diags[0].entity).toBe("shared");
|
|
197
|
+
expect(diags[0].message).toContain("allow-read");
|
|
198
|
+
expect(diags[0].message).toContain("allow-list");
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("flags the same id emitted into two policy sets in one build", () => {
|
|
202
|
+
const one = JSON.stringify(set({ "allow-read": policy() }));
|
|
203
|
+
const clash = makePostSynthCtxFromFiles("cedar", {
|
|
204
|
+
"a.cedar.json": one,
|
|
205
|
+
"b.cedar.json": one,
|
|
206
|
+
});
|
|
207
|
+
const diags = cedc012.check(clash);
|
|
208
|
+
expect(diags).toHaveLength(1);
|
|
209
|
+
expect(diags[0].entity).toBe("allow-read");
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("passes distinct ids", () => {
|
|
213
|
+
expect(cedc012.check(ctx(CLEAN))).toHaveLength(0);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// ── CEDC013 ────────────────────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
describe("CEDC013: annotation hygiene", () => {
|
|
220
|
+
test("flags an @id annotation that disagrees with the policy id", () => {
|
|
221
|
+
const mismatch = { staticPolicies: { "allow-read": { ...policy(), annotations: { id: "something-else" } } } };
|
|
222
|
+
const diags = cedc013.check(createPostSynthContext({ cedar: mismatch }));
|
|
223
|
+
expect(diags).toHaveLength(1);
|
|
224
|
+
expect(diags[0].checkId).toBe("CEDC013");
|
|
225
|
+
expect(diags[0].severity).toBe("warning");
|
|
226
|
+
expect(diags[0].message).toContain("something-else");
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("flags an empty annotation value", () => {
|
|
230
|
+
const diags = cedc013.check(ctx({ "allow-read": policy({ annotations: { doc: " " } }) }));
|
|
231
|
+
expect(diags).toHaveLength(1);
|
|
232
|
+
expect(diags[0].message).toContain("empty @doc");
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("flags a non-string annotation value", () => {
|
|
236
|
+
const diags = cedc013.check(ctx({ "allow-read": policy({ annotations: { doc: 7 } }) }));
|
|
237
|
+
expect(diags).toHaveLength(1);
|
|
238
|
+
expect(diags[0].message).toContain("non-string @doc");
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("a missing @id is not a finding — chant derives one", () => {
|
|
242
|
+
const derived = { staticPolicies: { "allow-read": policy() } };
|
|
243
|
+
expect(cedc013.check(createPostSynthContext({ cedar: derived }))).toHaveLength(0);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("passes matching ids and non-empty annotations", () => {
|
|
247
|
+
expect(cedc013.check(ctx(CLEAN))).toHaveLength(0);
|
|
248
|
+
});
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
// ── CEDC014 ────────────────────────────────────────────────────────
|
|
252
|
+
|
|
253
|
+
describe("CEDC014: scope entity references must be full UIDs", () => {
|
|
254
|
+
test("flags the empty-id degradation a non-UID reference produces", () => {
|
|
255
|
+
const diags = cedc014.check(
|
|
256
|
+
ctx({ "allow-read": policy({ principal: { op: "==", entity: uid("alice", "") } }) }),
|
|
257
|
+
);
|
|
258
|
+
expect(diags).toHaveLength(1);
|
|
259
|
+
expect(diags[0].checkId).toBe("CEDC014");
|
|
260
|
+
expect(diags[0].severity).toBe("error");
|
|
261
|
+
expect(diags[0].message).toContain("principal");
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
test("flags a degraded reference inside an in-list", () => {
|
|
265
|
+
const diags = cedc014.check(
|
|
266
|
+
ctx({
|
|
267
|
+
"allow-read": policy({
|
|
268
|
+
action: { op: "in", entities: [uid("Chant::Action", "read"), uid("write", "")] },
|
|
269
|
+
}),
|
|
270
|
+
}),
|
|
271
|
+
);
|
|
272
|
+
expect(diags).toHaveLength(1);
|
|
273
|
+
expect(diags[0].message).toContain("action");
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test("flags a degraded reference under an `is T in E` constraint", () => {
|
|
277
|
+
const diags = cedc014.check(
|
|
278
|
+
ctx({
|
|
279
|
+
"allow-read": policy({
|
|
280
|
+
resource: { op: "is", entity_type: "Chant::Repo", in: { entity: uid("core", "") } },
|
|
281
|
+
}),
|
|
282
|
+
}),
|
|
283
|
+
);
|
|
284
|
+
expect(diags).toHaveLength(1);
|
|
285
|
+
expect(diags[0].message).toContain("resource");
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("passes well-formed UIDs and unconstrained scopes", () => {
|
|
289
|
+
expect(cedc014.check(ctx(CLEAN))).toHaveLength(0);
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
// ── CEDE010 ────────────────────────────────────────────────────────
|
|
294
|
+
|
|
295
|
+
describe("CEDE010: the policy set validates against the schema", () => {
|
|
296
|
+
test("flags a policy naming an entity type the schema does not define", () => {
|
|
297
|
+
const diags = cede010.check(
|
|
298
|
+
ctxWithSchema({
|
|
299
|
+
ghost: policy({
|
|
300
|
+
principal: { op: "is", entity_type: "Chant::Ghost" },
|
|
301
|
+
action: { op: "==", entity: uid("Chant::Action", "read") },
|
|
302
|
+
resource: { op: "is", entity_type: "Chant::Repo" },
|
|
303
|
+
}),
|
|
304
|
+
}),
|
|
305
|
+
);
|
|
306
|
+
expect(diags.length).toBeGreaterThanOrEqual(1);
|
|
307
|
+
expect(diags[0].checkId).toBe("CEDE010");
|
|
308
|
+
expect(diags[0].severity).toBe("error");
|
|
309
|
+
expect(diags.map((d) => d.message).join(" ")).toContain("Ghost");
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
test("flags a policy naming an attribute the schema does not define", () => {
|
|
313
|
+
const diags = cede010.check(
|
|
314
|
+
ctxWithSchema({
|
|
315
|
+
typo: policy({
|
|
316
|
+
principal: { op: "is", entity_type: "Chant::User" },
|
|
317
|
+
action: { op: "==", entity: uid("Chant::Action", "read") },
|
|
318
|
+
resource: { op: "is", entity_type: "Chant::Repo" },
|
|
319
|
+
conditions: [{ kind: "when", body: { __expr: "resource.frozn == true" } }],
|
|
320
|
+
}),
|
|
321
|
+
}),
|
|
322
|
+
);
|
|
323
|
+
expect(diags.length).toBeGreaterThanOrEqual(1);
|
|
324
|
+
expect(diags[0].message).toContain("frozn");
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
test("reports validation errors in a stable order (the wasm's own order is not)", () => {
|
|
328
|
+
const noisy = ctxWithSchema({
|
|
329
|
+
"ghost-a": policy({
|
|
330
|
+
principal: { op: "is", entity_type: "Chant::Ghost" },
|
|
331
|
+
action: { op: "==", entity: uid("Chant::Action", "read") },
|
|
332
|
+
resource: { op: "is", entity_type: "Chant::Repo" },
|
|
333
|
+
}),
|
|
334
|
+
"ghost-b": policy({
|
|
335
|
+
principal: { op: "is", entity_type: "Chant::Phantom" },
|
|
336
|
+
action: { op: "==", entity: uid("Chant::Action", "write") },
|
|
337
|
+
resource: { op: "is", entity_type: "Chant::Repo" },
|
|
338
|
+
}),
|
|
339
|
+
});
|
|
340
|
+
const runs = Array.from({ length: 8 }, () => cede010.check(noisy).map((d) => d.message));
|
|
341
|
+
for (const run of runs) expect(run).toEqual(runs[0]);
|
|
342
|
+
expect(runs[0].length).toBeGreaterThanOrEqual(2);
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
test("advises rather than passing silently when no schema was emitted", () => {
|
|
346
|
+
const diags = cede010.check(ctx(CLEAN));
|
|
347
|
+
expect(diags).toHaveLength(1);
|
|
348
|
+
expect(diags[0].severity).toBe("info");
|
|
349
|
+
expect(diags[0].message).toContain("No Cedar schema");
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test("passes a policy set the validator accepts", () => {
|
|
353
|
+
expect(cede010.check(ctxWithSchema(CLEAN))).toHaveLength(0);
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// ── CEDE011 ────────────────────────────────────────────────────────
|
|
358
|
+
|
|
359
|
+
describe("CEDE011: the validator's warnings", () => {
|
|
360
|
+
test("flags an impossible policy", () => {
|
|
361
|
+
const diags = cede011.check(
|
|
362
|
+
ctxWithSchema({
|
|
363
|
+
impossible: policy({
|
|
364
|
+
principal: { op: "is", entity_type: "Chant::User" },
|
|
365
|
+
action: { op: "==", entity: uid("Chant::Action", "read") },
|
|
366
|
+
resource: { op: "is", entity_type: "Chant::Repo" },
|
|
367
|
+
conditions: [{ kind: "when", body: { __expr: "1 == 2" } }],
|
|
368
|
+
}),
|
|
369
|
+
}),
|
|
370
|
+
);
|
|
371
|
+
expect(diags.length).toBeGreaterThanOrEqual(1);
|
|
372
|
+
expect(diags[0].checkId).toBe("CEDE011");
|
|
373
|
+
expect(diags[0].severity).toBe("warning");
|
|
374
|
+
expect(diags[0].message).toContain("impossible");
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
test("says nothing about a policy set the validator is happy with", () => {
|
|
378
|
+
expect(cede011.check(ctxWithSchema(CLEAN))).toHaveLength(0);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
test("stays quiet when there is no schema — CEDE010 owns that advisory", () => {
|
|
382
|
+
expect(cede011.check(ctx(CLEAN))).toHaveLength(0);
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
// ── CEDS010 ────────────────────────────────────────────────────────
|
|
387
|
+
|
|
388
|
+
describe("CEDS010: the bare-permit wall", () => {
|
|
389
|
+
test("errors on a bare permit in a production build", () => {
|
|
390
|
+
const diags = ceds010.check(ctx({ "allow-all": policy() }, "prod"));
|
|
391
|
+
expect(diags).toHaveLength(1);
|
|
392
|
+
expect(diags[0].checkId).toBe("CEDS010");
|
|
393
|
+
expect(diags[0].severity).toBe("error");
|
|
394
|
+
expect(diags[0].entity).toBe("allow-all");
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
test("warns on the same policy outside production", () => {
|
|
398
|
+
const diags = ceds010.check(ctx({ "allow-all": policy() }));
|
|
399
|
+
expect(diags).toHaveLength(1);
|
|
400
|
+
expect(diags[0].severity).toBe("warning");
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
test("a forbid over everything is not the finding — a deny-all is safe", () => {
|
|
404
|
+
expect(ceds010.check(ctx({ "deny-all": policy({ effect: "forbid" }) }, "prod"))).toHaveLength(0);
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
test("a single when guard is enough to take it out of scope", () => {
|
|
408
|
+
const guarded = ctx(
|
|
409
|
+
{ "allow-all": policy({ conditions: [{ kind: "when", body: { __expr: "context.mfa" } }] }) },
|
|
410
|
+
"prod",
|
|
411
|
+
);
|
|
412
|
+
expect(ceds010.check(guarded)).toHaveLength(0);
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
test("passes a constrained permit", () => {
|
|
416
|
+
expect(ceds010.check(ctx(CLEAN, "prod"))).toHaveLength(0);
|
|
417
|
+
});
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
// ── CEDS011 ────────────────────────────────────────────────────────
|
|
421
|
+
|
|
422
|
+
describe("CEDS011: a policy set with no forbid", () => {
|
|
423
|
+
test("warns once for a permit-only set", () => {
|
|
424
|
+
const diags = ceds011.check(
|
|
425
|
+
ctx({
|
|
426
|
+
"allow-read": policy({ principal: { op: "in", entity: uid("Chant::Group", "admins") } }),
|
|
427
|
+
"allow-write": policy({ principal: { op: "in", entity: uid("Chant::Group", "admins") } }),
|
|
428
|
+
}),
|
|
429
|
+
);
|
|
430
|
+
expect(diags).toHaveLength(1);
|
|
431
|
+
expect(diags[0].checkId).toBe("CEDS011");
|
|
432
|
+
expect(diags[0].severity).toBe("warning");
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
test("passes a set carrying a forbid", () => {
|
|
436
|
+
expect(ceds011.check(ctx(CLEAN))).toHaveLength(0);
|
|
437
|
+
});
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
// ── CEDS012 ────────────────────────────────────────────────────────
|
|
441
|
+
|
|
442
|
+
describe("CEDS012: an unconstrained action scope", () => {
|
|
443
|
+
test("warns on a permit that constrains the principal but not the action", () => {
|
|
444
|
+
const diags = ceds012.check(
|
|
445
|
+
ctx({
|
|
446
|
+
"allow-admins": policy({
|
|
447
|
+
principal: { op: "in", entity: uid("Chant::Group", "admins") },
|
|
448
|
+
}),
|
|
449
|
+
}),
|
|
450
|
+
);
|
|
451
|
+
expect(diags).toHaveLength(1);
|
|
452
|
+
expect(diags[0].checkId).toBe("CEDS012");
|
|
453
|
+
expect(diags[0].severity).toBe("warning");
|
|
454
|
+
expect(diags[0].entity).toBe("allow-admins");
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
test("leaves the fully-bare permit to CEDS010", () => {
|
|
458
|
+
expect(ceds012.check(ctx({ "allow-all": policy() }))).toHaveLength(0);
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
test("says nothing about a forbid over every action", () => {
|
|
462
|
+
const denyAll = ctx({
|
|
463
|
+
"forbid-frozen": policy({
|
|
464
|
+
effect: "forbid",
|
|
465
|
+
resource: { op: "is", entity_type: "Chant::Repo" },
|
|
466
|
+
}),
|
|
467
|
+
});
|
|
468
|
+
expect(ceds012.check(denyAll)).toHaveLength(0);
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
test("passes a permit naming its actions", () => {
|
|
472
|
+
expect(ceds012.check(ctx(CLEAN))).toHaveLength(0);
|
|
473
|
+
});
|
|
474
|
+
});
|