@intentius/chant-lexicon-cedar 0.44.8 → 0.44.9
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 +58 -0
- package/dist/codegen/package.d.ts.map +1 -1
- package/dist/config.d.ts +25 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/dogwood/cli.d.ts +206 -0
- package/dist/dogwood/cli.d.ts.map +1 -0
- package/dist/dogwood/event-schema.d.ts +161 -0
- package/dist/dogwood/event-schema.d.ts.map +1 -0
- package/dist/dogwood/index.d.ts +33 -0
- package/dist/dogwood/index.d.ts.map +1 -0
- package/dist/dogwood/macros.d.ts +96 -0
- package/dist/dogwood/macros.d.ts.map +1 -0
- package/dist/dogwood/policy.d.ts +120 -0
- package/dist/dogwood/policy.d.ts.map +1 -0
- package/dist/dogwood/scan.d.ts +109 -0
- package/dist/dogwood/scan.d.ts.map +1 -0
- package/dist/dogwood/serialize.d.ts +46 -0
- package/dist/dogwood/serialize.d.ts.map +1 -0
- package/dist/dogwood/temporal.d.ts +259 -0
- package/dist/dogwood/temporal.d.ts.map +1 -0
- package/dist/dogwood/upstream.d.ts +41 -0
- package/dist/dogwood/upstream.d.ts.map +1 -0
- package/dist/dogwood/window.d.ts +73 -0
- package/dist/dogwood/window.d.ts.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +13 -3
- package/dist/lint/audit-catalog.d.ts.map +1 -1
- package/dist/lint/post-synth/dogwood-helpers.d.ts +63 -0
- package/dist/lint/post-synth/dogwood-helpers.d.ts.map +1 -0
- package/dist/lint/post-synth/dwdc010.d.ts +25 -0
- package/dist/lint/post-synth/dwdc010.d.ts.map +1 -0
- package/dist/lint/post-synth/dwdc011.d.ts +19 -0
- package/dist/lint/post-synth/dwdc011.d.ts.map +1 -0
- package/dist/lint/post-synth/dwdc012.d.ts +21 -0
- package/dist/lint/post-synth/dwdc012.d.ts.map +1 -0
- package/dist/lint/post-synth/dwde010.d.ts +32 -0
- package/dist/lint/post-synth/dwde010.d.ts.map +1 -0
- package/dist/lint/post-synth/dwde011.d.ts +33 -0
- package/dist/lint/post-synth/dwde011.d.ts.map +1 -0
- package/dist/lint/post-synth/dwds010.d.ts +24 -0
- package/dist/lint/post-synth/dwds010.d.ts.map +1 -0
- package/dist/lint/post-synth/index.d.ts.map +1 -1
- package/dist/manifest.json +1 -1
- package/dist/okf/index.md +6 -0
- package/dist/okf/rules/DWDC010.md +11 -0
- package/dist/okf/rules/DWDC011.md +11 -0
- package/dist/okf/rules/DWDC012.md +11 -0
- package/dist/okf/rules/DWDE010.md +11 -0
- package/dist/okf/rules/DWDE011.md +11 -0
- package/dist/okf/rules/DWDS010.md +11 -0
- package/dist/policy-text.d.ts +53 -0
- package/dist/policy-text.d.ts.map +1 -0
- package/dist/rules/dogwood-helpers.ts +139 -0
- package/dist/rules/dwdc010.ts +62 -0
- package/dist/rules/dwdc011.ts +61 -0
- package/dist/rules/dwdc012.ts +46 -0
- package/dist/rules/dwde010.ts +130 -0
- package/dist/rules/dwde011.ts +108 -0
- package/dist/rules/dwds010.ts +46 -0
- package/dist/serializer.d.ts +10 -18
- package/dist/serializer.d.ts.map +1 -1
- package/dist/skills/chant-cedar-authoring.md +180 -0
- package/dist/skills/chant-cedar-avp-embedding.md +125 -0
- package/dist/skills/chant-cedar-meta-policy.md +119 -0
- package/package.json +2 -2
- package/src/codegen/package.ts +3 -2
- package/src/config.test.ts +12 -0
- package/src/config.ts +28 -0
- package/src/dogwood/cli.test.ts +392 -0
- package/src/dogwood/cli.ts +545 -0
- package/src/dogwood/event-schema.test.ts +218 -0
- package/src/dogwood/event-schema.ts +318 -0
- package/src/dogwood/index.ts +198 -0
- package/src/dogwood/macros.test.ts +104 -0
- package/src/dogwood/macros.ts +229 -0
- package/src/dogwood/policy.test.ts +94 -0
- package/src/dogwood/policy.ts +141 -0
- package/src/dogwood/scan.ts +287 -0
- package/src/dogwood/serialize.test.ts +331 -0
- package/src/dogwood/serialize.ts +209 -0
- package/src/dogwood/temporal.test.ts +272 -0
- package/src/dogwood/temporal.ts +592 -0
- package/src/dogwood/testdata/custom-kinds.dwschema +17 -0
- package/src/dogwood/testdata/default-macros.dw +23 -0
- package/src/dogwood/testdata/lowered-read-after-login.json +13 -0
- package/src/dogwood/testdata/max-window-raised.dwschema +25 -0
- package/src/dogwood/testdata/pinned.dwschema +31 -0
- package/src/dogwood/testdata/read-after-login.cedarschema +20 -0
- package/src/dogwood/testdata/read-after-login.dw +17 -0
- package/src/dogwood/testdata/temporal-policies.dw +53 -0
- package/src/dogwood/upstream.ts +41 -0
- package/src/dogwood/window.ts +124 -0
- package/src/index.ts +24 -0
- package/src/lint/audit-catalog.ts +56 -0
- package/src/lint/post-synth/dogwood-helpers.ts +139 -0
- package/src/lint/post-synth/dwd-post-synth.test.ts +256 -0
- package/src/lint/post-synth/dwdc010.ts +62 -0
- package/src/lint/post-synth/dwdc011.ts +61 -0
- package/src/lint/post-synth/dwdc012.ts +46 -0
- package/src/lint/post-synth/dwde-post-synth.test.ts +368 -0
- package/src/lint/post-synth/dwde010.ts +130 -0
- package/src/lint/post-synth/dwde011.ts +108 -0
- package/src/lint/post-synth/dwds010.ts +46 -0
- package/src/lint/post-synth/index.ts +12 -0
- package/src/lint/post-synth/post-synth.test.ts +7 -3
- package/src/policy-text.ts +128 -0
- package/src/serializer.ts +71 -109
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CLI-gated DWDE checks (#1659).
|
|
3
|
+
*
|
|
4
|
+
* Every case injects a runner, and the absent-binary case forces absence
|
|
5
|
+
* rather than hoping the machine has none — a developer with `dogwood` built
|
|
6
|
+
* locally must see the same result as CI. The one test that touches a real
|
|
7
|
+
* binary is opt-in and skipped by default; nothing in gating depends on it.
|
|
8
|
+
*
|
|
9
|
+
* The lowered fixture is adapted from
|
|
10
|
+
* dogwood-policy/dogwood@5063bcc2d6d6cf5024d1b0498e6cc8ef52cbcf0c (Apache-2.0),
|
|
11
|
+
* `dogwood-docs/examples/read_after_login` — the same bundle the #1657
|
|
12
|
+
* verification put through this exact path.
|
|
13
|
+
*/
|
|
14
|
+
import { describe, test, expect, afterEach } from "vitest";
|
|
15
|
+
import { readFileSync } from "fs";
|
|
16
|
+
import { join } from "path";
|
|
17
|
+
import { fileURLToPath } from "url";
|
|
18
|
+
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
19
|
+
import type { SerializerResult } from "@intentius/chant/serializer";
|
|
20
|
+
import { postSynthChecks } from "./index";
|
|
21
|
+
import { dwde010 } from "./dwde010";
|
|
22
|
+
import { dwde011 } from "./dwde011";
|
|
23
|
+
import { looksLikeMacroLibrary, planDogwoodRuns } from "./dogwood-helpers";
|
|
24
|
+
import { cedarAuditCatalog } from "../audit-catalog";
|
|
25
|
+
import { configureDogwoodCli, findDogwoodBinary, resetDogwoodCli, type DogwoodRun } from "../../dogwood/cli";
|
|
26
|
+
|
|
27
|
+
const TESTDATA = join(fileURLToPath(new URL("../..", import.meta.url)), "dogwood/testdata");
|
|
28
|
+
|
|
29
|
+
const POLICIES = readFileSync(join(TESTDATA, "read-after-login.dw"), "utf-8");
|
|
30
|
+
const ACTION_SCHEMA = readFileSync(join(TESTDATA, "read-after-login.cedarschema"), "utf-8");
|
|
31
|
+
const EVENT_SCHEMA = readFileSync(join(TESTDATA, "pinned.dwschema"), "utf-8");
|
|
32
|
+
const MACROS = readFileSync(join(TESTDATA, "default-macros.dw"), "utf-8");
|
|
33
|
+
const LOWERED = JSON.parse(readFileSync(join(TESTDATA, "lowered-read-after-login.json"), "utf-8")) as Record<
|
|
34
|
+
string,
|
|
35
|
+
unknown
|
|
36
|
+
>;
|
|
37
|
+
|
|
38
|
+
afterEach(() => resetDogwoodCli());
|
|
39
|
+
|
|
40
|
+
function ctxOf(files: Record<string, string>): PostSynthContext {
|
|
41
|
+
const output: SerializerResult = { primary: "", files };
|
|
42
|
+
const outputs = new Map<string, string | SerializerResult>([["cedar", output]]);
|
|
43
|
+
return {
|
|
44
|
+
outputs,
|
|
45
|
+
entities: new Map(),
|
|
46
|
+
buildResult: { outputs, entities: new Map(), warnings: [], errors: [], sourceFileCount: 1 },
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** The shape a real build emits: policies, action schema, event schema, macros. */
|
|
51
|
+
function fullBuild(): PostSynthContext {
|
|
52
|
+
return ctxOf({
|
|
53
|
+
"policies.dw": POLICIES,
|
|
54
|
+
"macros.dw": MACROS,
|
|
55
|
+
"app.cedarschema": ACTION_SCHEMA,
|
|
56
|
+
"events.dwschema": EVENT_SCHEMA,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function run(over: Partial<DogwoodRun>): DogwoodRun {
|
|
61
|
+
return { status: 0, stdout: "", stderr: "", ...over };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const PASSING = run({
|
|
65
|
+
status: 0,
|
|
66
|
+
stdout: JSON.stringify({ passed: true, passed_without_warnings: true, errors: [], warnings: [] }),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
/** Answer `validate` one way and `lower` another, from the subcommand argv[0]. */
|
|
70
|
+
function bySubcommand(answers: Record<string, DogwoodRun>) {
|
|
71
|
+
return (_binary: string, args: string[]): DogwoodRun => answers[args[0]] ?? run({ status: 0, stdout: "" });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── The planner ────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
describe("planning what to run", () => {
|
|
77
|
+
test("a macro library is told from a policy set by structure, not by filename", () => {
|
|
78
|
+
expect(looksLikeMacroLibrary(MACROS)).toBe(true);
|
|
79
|
+
expect(looksLikeMacroLibrary(POLICIES)).toBe(false);
|
|
80
|
+
// A library under a non-default filename is still a library.
|
|
81
|
+
expect(looksLikeMacroLibrary("def temporal once(?w, ?s) { formerly within ?w ?s };")).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("one bundle per policy set, with the macro libraries folded into one --macros file", () => {
|
|
85
|
+
const plan = planDogwoodRuns(fullBuild());
|
|
86
|
+
expect(plan.hasPolicies).toBe(true);
|
|
87
|
+
expect(plan.bundles).toHaveLength(1);
|
|
88
|
+
const [only] = plan.bundles;
|
|
89
|
+
expect(only.source).toBe("policies.dw");
|
|
90
|
+
expect(only.policySchemaSource).toBe("app.cedarschema");
|
|
91
|
+
expect(only.eventSchemaSource).toBe("events.dwschema");
|
|
92
|
+
expect(only.bundle.macros).toContain("def temporal");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("several event schemas mean several runs, each naming its own schema", () => {
|
|
96
|
+
const ctx = ctxOf({
|
|
97
|
+
"policies.dw": POLICIES,
|
|
98
|
+
"app.cedarschema": ACTION_SCHEMA,
|
|
99
|
+
"events.dwschema": EVENT_SCHEMA,
|
|
100
|
+
"gateway.dwschema": EVENT_SCHEMA,
|
|
101
|
+
});
|
|
102
|
+
const plan = planDogwoodRuns(ctx);
|
|
103
|
+
expect(plan.bundles.map((b) => b.eventSchemaSource)).toEqual(["events.dwschema", "gateway.dwschema"]);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("no .dw policy set means nothing to plan, even with a schema emitted", () => {
|
|
107
|
+
expect(planDogwoodRuns(ctxOf({ "events.dwschema": EVENT_SCHEMA })).hasPolicies).toBe(false);
|
|
108
|
+
expect(planDogwoodRuns(ctxOf({ "macros.dw": MACROS })).hasPolicies).toBe(false);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test("policies with no action schema are blocked, because --policy-schema is not optional", () => {
|
|
112
|
+
const plan = planDogwoodRuns(ctxOf({ "policies.dw": POLICIES }));
|
|
113
|
+
expect(plan.hasPolicies).toBe(true);
|
|
114
|
+
expect(plan.bundles).toEqual([]);
|
|
115
|
+
expect(plan.blocked).toContain("--policy-schema");
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
// ── DWDE010 ────────────────────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
describe("DWDE010 — full .dw validation, when the binary is there", () => {
|
|
122
|
+
test("a passing validate is silent", () => {
|
|
123
|
+
configureDogwoodCli({ binary: "/opt/dogwood", runner: () => PASSING });
|
|
124
|
+
expect(dwde010.check(fullBuild())).toEqual([]);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("a rejection becomes one error per finding, with the code and the byte span", () => {
|
|
128
|
+
const rejected = run({
|
|
129
|
+
status: 2,
|
|
130
|
+
stdout: JSON.stringify({
|
|
131
|
+
passed: false,
|
|
132
|
+
passed_without_warnings: false,
|
|
133
|
+
errors: [
|
|
134
|
+
{
|
|
135
|
+
severity: "error",
|
|
136
|
+
code: "extension",
|
|
137
|
+
message:
|
|
138
|
+
'predicate `Drupe::Action::"NoSuchAction"::request` does not name a declared event (no event kind `request` derived for action `NoSuchAction`)',
|
|
139
|
+
labels: [{ start: 210, len: 46 }],
|
|
140
|
+
spanned: true,
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
warnings: [],
|
|
144
|
+
}),
|
|
145
|
+
});
|
|
146
|
+
configureDogwoodCli({ binary: "/opt/dogwood", runner: () => rejected });
|
|
147
|
+
|
|
148
|
+
const [finding, ...rest] = dwde010.check(fullBuild());
|
|
149
|
+
expect(rest).toEqual([]);
|
|
150
|
+
expect(finding.severity).toBe("error");
|
|
151
|
+
expect(finding.checkId).toBe("DWDE010");
|
|
152
|
+
expect(finding.message).toContain("[extension]");
|
|
153
|
+
expect(finding.message).toContain("bytes 210-256");
|
|
154
|
+
expect(finding.message).toContain('"policies.dw"');
|
|
155
|
+
expect(finding.message).toContain('against "app.cedarschema"');
|
|
156
|
+
expect(finding.entity).toBe("policies.dw");
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("a fatal parse error comes through the other JSON shape and is still an error", () => {
|
|
160
|
+
const fatal = run({
|
|
161
|
+
status: 2,
|
|
162
|
+
stdout: JSON.stringify({
|
|
163
|
+
severity: "error",
|
|
164
|
+
message: "unknown macro `once`",
|
|
165
|
+
labels: [{ start: 12, len: 4 }],
|
|
166
|
+
spanned: true,
|
|
167
|
+
related: [{ severity: "error", message: "a second parse error", spanned: false }],
|
|
168
|
+
}),
|
|
169
|
+
});
|
|
170
|
+
configureDogwoodCli({ binary: "/opt/dogwood", runner: () => fatal });
|
|
171
|
+
|
|
172
|
+
const findings = dwde010.check(fullBuild());
|
|
173
|
+
expect(findings).toHaveLength(2);
|
|
174
|
+
expect(findings.every((f) => f.severity === "error")).toBe(true);
|
|
175
|
+
expect(findings[0].message).toContain("could not be parsed or lowered");
|
|
176
|
+
expect(findings[0].message).toContain("unknown macro `once`");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("upstream's warnings ride along at warning severity and do not fail the build", () => {
|
|
180
|
+
const warned = run({
|
|
181
|
+
status: 0,
|
|
182
|
+
stdout: JSON.stringify({
|
|
183
|
+
passed: true,
|
|
184
|
+
passed_without_warnings: false,
|
|
185
|
+
errors: [],
|
|
186
|
+
warnings: [{ severity: "warning", message: "policy is impossible", labels: [], spanned: false }],
|
|
187
|
+
}),
|
|
188
|
+
});
|
|
189
|
+
configureDogwoodCli({ binary: "/opt/dogwood", runner: () => warned });
|
|
190
|
+
|
|
191
|
+
const [finding] = dwde010.check(fullBuild());
|
|
192
|
+
expect(finding.severity).toBe("warning");
|
|
193
|
+
expect(finding.message).toContain("policy is impossible");
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
test("the exit-2 ambiguity is a warning about the run, never an error about the policy", () => {
|
|
197
|
+
// An unknown flag and a rejected policy set share exit 2. Reading the code
|
|
198
|
+
// alone would turn a flag rename in an upstream sync into a failed build.
|
|
199
|
+
const usage = run({
|
|
200
|
+
status: 2,
|
|
201
|
+
stdout: "",
|
|
202
|
+
stderr: "error: unexpected argument '--nosuchflag' found",
|
|
203
|
+
});
|
|
204
|
+
configureDogwoodCli({ binary: "/opt/dogwood", runner: () => usage });
|
|
205
|
+
|
|
206
|
+
const [finding, ...rest] = dwde010.check(fullBuild());
|
|
207
|
+
expect(rest).toEqual([]);
|
|
208
|
+
expect(finding.severity).toBe("warning");
|
|
209
|
+
expect(finding.message).toContain("neither accepted nor rejected");
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("no binary is exactly one info advisory naming the binary and the issue", () => {
|
|
213
|
+
configureDogwoodCli({ binary: null });
|
|
214
|
+
const [finding, ...rest] = dwde010.check(fullBuild());
|
|
215
|
+
expect(rest).toEqual([]);
|
|
216
|
+
expect(finding.severity).toBe("info");
|
|
217
|
+
expect(finding.message).toContain("dogwood");
|
|
218
|
+
expect(finding.message).toContain("chant #1659");
|
|
219
|
+
expect(finding.message).toContain("CHANT_DOGWOOD_BINARY");
|
|
220
|
+
expect(finding.lexicon).toBe("cedar");
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("the advisory does not fire on a build with no .dw output at all", () => {
|
|
224
|
+
configureDogwoodCli({ binary: null });
|
|
225
|
+
expect(dwde010.check(ctxOf({ "policies.cedar.json": "{}" }))).toEqual([]);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
test("a binary with nothing to validate against says so rather than passing quietly", () => {
|
|
229
|
+
configureDogwoodCli({ binary: "/opt/dogwood", runner: () => PASSING });
|
|
230
|
+
const [finding] = dwde010.check(ctxOf({ "policies.dw": POLICIES }));
|
|
231
|
+
expect(finding.severity).toBe("info");
|
|
232
|
+
expect(finding.message).toContain("--policy-schema");
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
// ── DWDE011 ────────────────────────────────────────────────────────
|
|
237
|
+
|
|
238
|
+
describe("DWDE011 — the lowered Cedar through Cedar's own validator", () => {
|
|
239
|
+
test("the lowered read_after_login bundle validates clean end to end", () => {
|
|
240
|
+
configureDogwoodCli({
|
|
241
|
+
binary: "/opt/dogwood",
|
|
242
|
+
runner: bySubcommand({
|
|
243
|
+
validate: PASSING,
|
|
244
|
+
lower: run({ status: 0, stdout: JSON.stringify(LOWERED) }),
|
|
245
|
+
}),
|
|
246
|
+
});
|
|
247
|
+
expect(dwde011.check(fullBuild())).toEqual([]);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test("a lowered body naming a slot the augmented schema lacks fails Cedar validation", () => {
|
|
251
|
+
// The same artifacts with the policy pointed at a context field the
|
|
252
|
+
// augmented schema never grew — what a lowering/schema mismatch looks like
|
|
253
|
+
// from Cedar's side.
|
|
254
|
+
const drifted = {
|
|
255
|
+
...LOWERED,
|
|
256
|
+
cedar_policies: (LOWERED.cedar_policies as string).replace(
|
|
257
|
+
"policy_0__temporal_0",
|
|
258
|
+
"policy_9__temporal_9",
|
|
259
|
+
),
|
|
260
|
+
};
|
|
261
|
+
configureDogwoodCli({
|
|
262
|
+
binary: "/opt/dogwood",
|
|
263
|
+
runner: bySubcommand({ validate: PASSING, lower: run({ status: 0, stdout: JSON.stringify(drifted) }) }),
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
const findings = dwde011.check(fullBuild());
|
|
267
|
+
expect(findings.length).toBeGreaterThan(0);
|
|
268
|
+
expect(findings[0].severity).toBe("error");
|
|
269
|
+
expect(findings[0].checkId).toBe("DWDE011");
|
|
270
|
+
expect(findings[0].message).toContain("fails Cedar validation");
|
|
271
|
+
expect(findings[0].entity).toBe("policies.dw");
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("findings are sorted, so a build's output does not shuffle between runs", () => {
|
|
275
|
+
const twoBad = {
|
|
276
|
+
...LOWERED,
|
|
277
|
+
cedar_policies: `permit (principal, action == Drupe::Action::"Read", resource) when { context.zzz };\npermit (principal, action == Drupe::Action::"Read", resource) when { context.aaa };\n`,
|
|
278
|
+
};
|
|
279
|
+
configureDogwoodCli({
|
|
280
|
+
binary: "/opt/dogwood",
|
|
281
|
+
runner: bySubcommand({ validate: PASSING, lower: run({ status: 0, stdout: JSON.stringify(twoBad) }) }),
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
const messages = dwde011.check(fullBuild()).map((f) => f.message);
|
|
285
|
+
expect(messages).toEqual([...messages].sort());
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
test("silent when the binary is absent — DWDE010's advisory covers that once", () => {
|
|
289
|
+
configureDogwoodCli({ binary: null });
|
|
290
|
+
expect(dwde011.check(fullBuild())).toEqual([]);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test("silent when no action schema was emitted, for the same reason", () => {
|
|
294
|
+
configureDogwoodCli({ binary: "/opt/dogwood", runner: () => PASSING });
|
|
295
|
+
expect(dwde011.check(ctxOf({ "policies.dw": POLICIES }))).toEqual([]);
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
test("a fatal lower stays quiet — the validate leg owns the parse/lower channel", () => {
|
|
299
|
+
configureDogwoodCli({
|
|
300
|
+
binary: "/opt/dogwood",
|
|
301
|
+
runner: bySubcommand({
|
|
302
|
+
validate: PASSING,
|
|
303
|
+
lower: run({
|
|
304
|
+
status: 2,
|
|
305
|
+
stdout: JSON.stringify({ severity: "error", message: "unknown macro `once`", spanned: false }),
|
|
306
|
+
}),
|
|
307
|
+
}),
|
|
308
|
+
});
|
|
309
|
+
expect(dwde011.check(fullBuild())).toEqual([]);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
test("a lower that could not be run is a warning about the run", () => {
|
|
313
|
+
configureDogwoodCli({
|
|
314
|
+
binary: "/opt/dogwood",
|
|
315
|
+
runner: bySubcommand({ validate: PASSING, lower: run({ status: 2, stdout: "", stderr: "usage" }) }),
|
|
316
|
+
});
|
|
317
|
+
const [finding] = dwde011.check(fullBuild());
|
|
318
|
+
expect(finding.severity).toBe("warning");
|
|
319
|
+
expect(finding.message).toContain("not checked by Cedar's validator");
|
|
320
|
+
});
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
// ── Registration ───────────────────────────────────────────────────
|
|
324
|
+
|
|
325
|
+
describe("registration", () => {
|
|
326
|
+
test("both checks reach the committed barrel", () => {
|
|
327
|
+
const ids = postSynthChecks.map((c) => c.id);
|
|
328
|
+
expect(ids).toContain("DWDE010");
|
|
329
|
+
expect(ids).toContain("DWDE011");
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
test("both are catalogued, so `chant audit` can title them", () => {
|
|
333
|
+
for (const id of ["DWDE010", "DWDE011"]) {
|
|
334
|
+
const meta = cedarAuditCatalog[id];
|
|
335
|
+
expect(meta, `${id} is catalogued`).toBeDefined();
|
|
336
|
+
expect(meta.title.length).toBeGreaterThan(0);
|
|
337
|
+
expect(meta.remediation.length).toBeGreaterThan(0);
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
test("the helper module contributes no check of its own", () => {
|
|
342
|
+
// `dogwood-helpers.ts` is excluded from discovery by the "helper" filename
|
|
343
|
+
// filter, the same way `wasm-helpers.ts` is.
|
|
344
|
+
expect(postSynthChecks.filter((c) => c.id.startsWith("DWD"))).toHaveLength(6);
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
// ── Opt-in: a real binary ──────────────────────────────────────────
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Runs only when `CHANT_DOGWOOD_E2E=1` *and* a binary resolves. Never gates:
|
|
352
|
+
* the epic's rule is that nothing in gating CI executes the dogwood binary,
|
|
353
|
+
* and this exists so someone who has built one can confirm the adapter reads
|
|
354
|
+
* the real thing the way the fixtures say it does.
|
|
355
|
+
*/
|
|
356
|
+
const haveRealBinary = process.env.CHANT_DOGWOOD_E2E === "1" && findDogwoodBinary() !== undefined;
|
|
357
|
+
|
|
358
|
+
describe.skipIf(!haveRealBinary)("against a real dogwood binary (opt-in)", () => {
|
|
359
|
+
test("the upstream read_after_login bundle validates clean", () => {
|
|
360
|
+
resetDogwoodCli();
|
|
361
|
+
expect(dwde010.check(fullBuild())).toEqual([]);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test("and its lowered Cedar validates clean through cedar-wasm", () => {
|
|
365
|
+
resetDogwoodCli();
|
|
366
|
+
expect(dwde011.check(fullBuild())).toEqual([]);
|
|
367
|
+
});
|
|
368
|
+
});
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DWDE010: the emitted `.dw` set validates clean under `dogwood validate`
|
|
3
|
+
*
|
|
4
|
+
* The CLI-gated half of the epic's validation split. Everything the DWDC walls
|
|
5
|
+
* ask is answerable in TypeScript and gates unconditionally; everything else —
|
|
6
|
+
* macro expansion, the temporal type checker, the Cedar body checked against
|
|
7
|
+
* the action schema *through upstream's own frontend* — needs upstream's Rust
|
|
8
|
+
* frontend, which ships as a binary and nothing else. No npm package, no wasm
|
|
9
|
+
* build, no bindings.
|
|
10
|
+
*
|
|
11
|
+
* So this check has two modes and says which one it is in:
|
|
12
|
+
*
|
|
13
|
+
* - **Binary present.** Runs `dogwood validate --format json` over each
|
|
14
|
+
* emitted policy set and reports every finding as an error. Byte-offset
|
|
15
|
+
* labels come through as byte offsets — see `../../dogwood/cli.ts` for why
|
|
16
|
+
* they are not converted to line/column.
|
|
17
|
+
* - **Binary absent.** Exactly one `info` finding naming the binary, where
|
|
18
|
+
* chant looked, and this issue. The epic's words are "an explicit,
|
|
19
|
+
* issue-linked exception, not a silent one" — a check that quietly passes
|
|
20
|
+
* when it could not run is claiming a guarantee it did not make, which is
|
|
21
|
+
* the same failure CEDE010's no-schema advisory exists to avoid.
|
|
22
|
+
*
|
|
23
|
+
* A run that could not be made at all — a spawn failure, an unknown flag, JSON
|
|
24
|
+
* in a shape this adapter does not know — is `warning`, not `error`. Upstream
|
|
25
|
+
* is a read-only squash-sync mirror with no tags and no changelog, and a flag
|
|
26
|
+
* rename in a sync would otherwise fail every build that has the binary
|
|
27
|
+
* installed. The adapter never reads exit 2 as "rejected" on its own for the
|
|
28
|
+
* same reason: clap spends that code on usage errors too.
|
|
29
|
+
*/
|
|
30
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
31
|
+
import {
|
|
32
|
+
DOGWOOD_BINARY_NAME,
|
|
33
|
+
DOGWOOD_SEARCH_ORDER,
|
|
34
|
+
findDogwoodBinary,
|
|
35
|
+
formatDogwoodDiagnostic,
|
|
36
|
+
runDogwoodValidate,
|
|
37
|
+
} from "../../dogwood/cli";
|
|
38
|
+
import { describeBundle, planDogwoodRuns } from "./dogwood-helpers";
|
|
39
|
+
|
|
40
|
+
export const dwde010: PostSynthCheck = {
|
|
41
|
+
id: "DWDE010",
|
|
42
|
+
description: "Emitted dogwood policy sets validate clean under `dogwood validate`, when the binary is available",
|
|
43
|
+
|
|
44
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
45
|
+
const plan = planDogwoodRuns(ctx);
|
|
46
|
+
if (!plan.hasPolicies) return [];
|
|
47
|
+
|
|
48
|
+
const binary = findDogwoodBinary();
|
|
49
|
+
if (!binary) {
|
|
50
|
+
return [
|
|
51
|
+
{
|
|
52
|
+
checkId: "DWDE010",
|
|
53
|
+
severity: "info",
|
|
54
|
+
message: `This build emitted dogwood .dw policies, but no \`${DOGWOOD_BINARY_NAME}\` binary was found, so full .dw validation did not run — the DWDC walls checked what TypeScript can answer and nothing checked macro expansion, the temporal type check, or the Cedar body against the action schema. chant looked at ${DOGWOOD_SEARCH_ORDER}. See chant #1659.`,
|
|
55
|
+
lexicon: plan.lexicon,
|
|
56
|
+
},
|
|
57
|
+
];
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (plan.blocked) {
|
|
61
|
+
return [
|
|
62
|
+
{
|
|
63
|
+
checkId: "DWDE010",
|
|
64
|
+
severity: "info",
|
|
65
|
+
message: `This build emitted dogwood .dw policies and \`${DOGWOOD_BINARY_NAME}\` is available, but ${plan.blocked}, so full .dw validation did not run. See chant #1659.`,
|
|
66
|
+
lexicon: plan.lexicon,
|
|
67
|
+
},
|
|
68
|
+
];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
72
|
+
|
|
73
|
+
for (const prepared of plan.bundles) {
|
|
74
|
+
const where = describeBundle(prepared);
|
|
75
|
+
const result = runDogwoodValidate(binary.path, prepared.bundle);
|
|
76
|
+
|
|
77
|
+
if (result.kind === "unusable") {
|
|
78
|
+
diagnostics.push({
|
|
79
|
+
checkId: "DWDE010",
|
|
80
|
+
severity: "warning",
|
|
81
|
+
message: `Dogwood policy set ${where} could not be validated — ${result.reason}. The policy set was neither accepted nor rejected.`,
|
|
82
|
+
entity: prepared.source,
|
|
83
|
+
lexicon: prepared.lexicon,
|
|
84
|
+
});
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (result.kind === "fatal") {
|
|
89
|
+
for (const finding of [result.error, ...result.related]) {
|
|
90
|
+
diagnostics.push({
|
|
91
|
+
checkId: "DWDE010",
|
|
92
|
+
severity: "error",
|
|
93
|
+
message: `Dogwood policy set ${where} could not be parsed or lowered: ${formatDogwoodDiagnostic(finding)}`,
|
|
94
|
+
entity: prepared.source,
|
|
95
|
+
lexicon: prepared.lexicon,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (result.kind === "rejected") {
|
|
102
|
+
for (const finding of result.errors) {
|
|
103
|
+
diagnostics.push({
|
|
104
|
+
checkId: "DWDE010",
|
|
105
|
+
severity: "error",
|
|
106
|
+
message: `Dogwood policy set ${where} fails \`dogwood validate\`: ${formatDogwoodDiagnostic(finding)}`,
|
|
107
|
+
entity: prepared.source,
|
|
108
|
+
lexicon: prepared.lexicon,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Upstream's own warnings ride along at warning severity. They do not
|
|
114
|
+
// fail `dogwood validate` (`passed` ignores them), and they should not
|
|
115
|
+
// fail a build either — but dropping them would throw away the half of
|
|
116
|
+
// the report that says a policy is legal and pointless.
|
|
117
|
+
for (const finding of result.warnings) {
|
|
118
|
+
diagnostics.push({
|
|
119
|
+
checkId: "DWDE010",
|
|
120
|
+
severity: "warning",
|
|
121
|
+
message: `Dogwood policy set ${where} draws a \`dogwood validate\` warning: ${formatDogwoodDiagnostic(finding)}`,
|
|
122
|
+
entity: prepared.source,
|
|
123
|
+
lexicon: prepared.lexicon,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return diagnostics;
|
|
129
|
+
},
|
|
130
|
+
};
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DWDE011: the lowered Cedar body validates clean under Cedar's own validator
|
|
3
|
+
*
|
|
4
|
+
* The epic's stated design for the non-temporal half: chant does not
|
|
5
|
+
* re-implement lowering — `dogwood lower` produces the analyzable plain-Cedar
|
|
6
|
+
* form, and a reimplementation would drift — but once that form exists, Cedar's
|
|
7
|
+
* own validator is the thing that judges it. The #1657 verification put all 86
|
|
8
|
+
* upstream example bundles through this exact path and every one parsed and
|
|
9
|
+
* validated clean in strict mode, which is what makes a finding here mean
|
|
10
|
+
* something: the lowered output is genuinely plain Cedar, so a validation error
|
|
11
|
+
* is the policy's, not the pipeline's.
|
|
12
|
+
*
|
|
13
|
+
* What this catches that DWDE010 does not is narrow but real. `dogwood
|
|
14
|
+
* validate` type-checks through upstream's vendored Cedar; this runs the
|
|
15
|
+
* *published* `@cedar-policy/cedar-wasm` over the same artifacts. A body that
|
|
16
|
+
* upstream's pinned Cedar accepts and the Cedar the rest of chant validates
|
|
17
|
+
* against rejects is exactly the drift the pin exists to make visible — and the
|
|
18
|
+
* augmented schema, with its hoisted `context.*` temporal slots, is the form a
|
|
19
|
+
* downstream Cedar policy store will actually receive.
|
|
20
|
+
*
|
|
21
|
+
* The cedar-wasm traps from #1648 all apply and are handled in
|
|
22
|
+
* `./wasm-helpers.ts`: `type: "success"` means validation *ran*, not that it
|
|
23
|
+
* passed; `validationErrors` comes back in a different order almost every call
|
|
24
|
+
* and is sorted before anything reads it; a malformed call throws rather than
|
|
25
|
+
* returning a failure answer.
|
|
26
|
+
*
|
|
27
|
+
* Silent when `lower` did not run — the binary is absent, or no action schema
|
|
28
|
+
* was emitted. Both cases are DWDE010's advisory to report, once, rather than
|
|
29
|
+
* twice in different words.
|
|
30
|
+
*/
|
|
31
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
32
|
+
import { findDogwoodBinary, runDogwoodLower } from "../../dogwood/cli";
|
|
33
|
+
import { describeBundle, planDogwoodRuns } from "./dogwood-helpers";
|
|
34
|
+
import { loadWasm, validatePolicySet } from "./wasm-helpers";
|
|
35
|
+
|
|
36
|
+
export const dwde011: PostSynthCheck = {
|
|
37
|
+
id: "DWDE011",
|
|
38
|
+
description: "The Cedar `dogwood lower` produces validates clean against the augmented schema (cedar-wasm validate)",
|
|
39
|
+
|
|
40
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
41
|
+
const plan = planDogwoodRuns(ctx);
|
|
42
|
+
if (!plan.hasPolicies || plan.blocked || plan.bundles.length === 0) return [];
|
|
43
|
+
|
|
44
|
+
const binary = findDogwoodBinary();
|
|
45
|
+
if (!binary) return [];
|
|
46
|
+
|
|
47
|
+
// A missing validator is not this check's finding to report; the cedar leg
|
|
48
|
+
// says so already through CEDC010.
|
|
49
|
+
const wasm = loadWasm();
|
|
50
|
+
if (!wasm) return [];
|
|
51
|
+
|
|
52
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
53
|
+
|
|
54
|
+
for (const prepared of plan.bundles) {
|
|
55
|
+
const where = describeBundle(prepared);
|
|
56
|
+
const lowered = runDogwoodLower(binary.path, prepared.bundle);
|
|
57
|
+
|
|
58
|
+
if (lowered.kind === "unusable") {
|
|
59
|
+
diagnostics.push({
|
|
60
|
+
checkId: "DWDE011",
|
|
61
|
+
severity: "warning",
|
|
62
|
+
message: `Dogwood policy set ${where} could not be lowered to Cedar — ${lowered.reason}. Its non-temporal body was not checked by Cedar's validator.`,
|
|
63
|
+
entity: prepared.source,
|
|
64
|
+
lexicon: prepared.lexicon,
|
|
65
|
+
});
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (lowered.kind === "fatal") {
|
|
70
|
+
// The same fatal DWDE010 reports from its own run. Saying it twice
|
|
71
|
+
// helps nobody, so this arm stays quiet and lets the validate leg own
|
|
72
|
+
// the parse/lower channel.
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const { cedarPolicies, cedarSchema } = lowered.value;
|
|
77
|
+
|
|
78
|
+
// The lowered policies go in as Cedar text. cedar-wasm synthesizes ids
|
|
79
|
+
// (`policy0`, `policy1`) for a bare-string set and does not read `@id`
|
|
80
|
+
// annotations as ids (#1648), so a finding names the .dw file it came
|
|
81
|
+
// from as well as the id the validator used.
|
|
82
|
+
const outcome = validatePolicySet(wasm, { staticPolicies: cedarPolicies }, cedarSchema);
|
|
83
|
+
|
|
84
|
+
if (outcome.failure) {
|
|
85
|
+
diagnostics.push({
|
|
86
|
+
checkId: "DWDE011",
|
|
87
|
+
severity: "error",
|
|
88
|
+
message: `The Cedar lowered from dogwood policy set ${where} could not be validated: ${outcome.failure}`,
|
|
89
|
+
entity: prepared.source,
|
|
90
|
+
lexicon: prepared.lexicon,
|
|
91
|
+
});
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
for (const finding of outcome.errors) {
|
|
96
|
+
diagnostics.push({
|
|
97
|
+
checkId: "DWDE011",
|
|
98
|
+
severity: "error",
|
|
99
|
+
message: `The Cedar lowered from dogwood policy set ${where} fails Cedar validation (lowered policy "${finding.policyId}"): ${finding.message}`,
|
|
100
|
+
entity: prepared.source,
|
|
101
|
+
lexicon: prepared.lexicon,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return diagnostics;
|
|
107
|
+
},
|
|
108
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DWDS010: an emitted event schema with no pinned field widens every predicate
|
|
3
|
+
*
|
|
4
|
+
* The #1657 verification is specific about this one. `ServiceSchema::defaults()`
|
|
5
|
+
* — what runs when nobody passes `--event-schema` — uses
|
|
6
|
+
* `configuration/event-schemas/pinned.dwschema`, which carries
|
|
7
|
+
* `pin callerPrincipal: principalType(A) = principal` on every kind. Every
|
|
8
|
+
* temporal predicate is therefore correlated to the deciding request's
|
|
9
|
+
* principal, and events from other principals are invisible.
|
|
10
|
+
*
|
|
11
|
+
* Supplying any event schema opts out of that default wholesale. So a schema
|
|
12
|
+
* emitted without a pin does not merely "not add" a correlation: it removes
|
|
13
|
+
* one the policy author very likely assumed, and every `formerly` in the set
|
|
14
|
+
* starts matching other principals' events. That is a legitimate design —
|
|
15
|
+
* cross-principal correlation is the reason to write your own schema — but it
|
|
16
|
+
* is a decision, and a decision nobody can see in a diff is one nobody made.
|
|
17
|
+
*
|
|
18
|
+
* Report-only: chant does not know which the author wanted, only that it
|
|
19
|
+
* should be said out loud. `defaultEventSchema({ pinCallerPrincipal: false })`
|
|
20
|
+
* also stamps the reasoning into the emitted file.
|
|
21
|
+
*/
|
|
22
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
23
|
+
import { dogwoodSchemaFiles, readEventSchema } from "../../dogwood/scan";
|
|
24
|
+
|
|
25
|
+
export const dwds010: PostSynthCheck = {
|
|
26
|
+
id: "DWDS010",
|
|
27
|
+
description: "An emitted dogwood event schema pins its temporal predicates to a request-side value",
|
|
28
|
+
|
|
29
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
30
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
31
|
+
|
|
32
|
+
for (const schema of dogwoodSchemaFiles(ctx)) {
|
|
33
|
+
const facts = readEventSchema(schema.text);
|
|
34
|
+
if (facts.kinds.length === 0 || facts.hasPin) continue;
|
|
35
|
+
diagnostics.push({
|
|
36
|
+
checkId: "DWDS010",
|
|
37
|
+
severity: "warning",
|
|
38
|
+
message: `Dogwood event schema "${schema.source}" declares no pinned field, so temporal predicates correlate across every principal — wider than upstream's default, which pins callerPrincipal to the deciding request's principal. Add a pinned field, or record why cross-principal correlation is wanted.`,
|
|
39
|
+
entity: schema.source,
|
|
40
|
+
lexicon: schema.lexicon,
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return diagnostics;
|
|
45
|
+
},
|
|
46
|
+
};
|
|
@@ -10,6 +10,12 @@ import { cede011 } from "./cede011";
|
|
|
10
10
|
import { ceds010 } from "./ceds010";
|
|
11
11
|
import { ceds011 } from "./ceds011";
|
|
12
12
|
import { ceds012 } from "./ceds012";
|
|
13
|
+
import { dwdc010 } from "./dwdc010";
|
|
14
|
+
import { dwdc011 } from "./dwdc011";
|
|
15
|
+
import { dwdc012 } from "./dwdc012";
|
|
16
|
+
import { dwde010 } from "./dwde010";
|
|
17
|
+
import { dwde011 } from "./dwde011";
|
|
18
|
+
import { dwds010 } from "./dwds010";
|
|
13
19
|
|
|
14
20
|
export const postSynthChecks: PostSynthCheck[] = [
|
|
15
21
|
cedc010,
|
|
@@ -22,4 +28,10 @@ export const postSynthChecks: PostSynthCheck[] = [
|
|
|
22
28
|
ceds010,
|
|
23
29
|
ceds011,
|
|
24
30
|
ceds012,
|
|
31
|
+
dwdc010,
|
|
32
|
+
dwdc011,
|
|
33
|
+
dwdc012,
|
|
34
|
+
dwde010,
|
|
35
|
+
dwde011,
|
|
36
|
+
dwds010,
|
|
25
37
|
];
|