@intentius/chant-lexicon-cedar 0.44.8 → 0.44.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +106 -2
- package/dist/agentcore/embed.d.ts +189 -0
- package/dist/agentcore/embed.d.ts.map +1 -0
- package/dist/agentcore/enforcement.d.ts +76 -0
- package/dist/agentcore/enforcement.d.ts.map +1 -0
- package/dist/agentcore/scan.d.ts +46 -0
- package/dist/agentcore/scan.d.ts.map +1 -0
- package/dist/codegen/docs-dogwood.d.ts +21 -0
- package/dist/codegen/docs-dogwood.d.ts.map +1 -0
- package/dist/codegen/docs.d.ts.map +1 -1
- 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 +247 -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 +39 -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/replay-activity.d.ts +196 -0
- package/dist/dogwood/replay-activity.d.ts.map +1 -0
- package/dist/dogwood/replay-op.d.ts +165 -0
- package/dist/dogwood/replay-op.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 +66 -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/trace.d.ts +215 -0
- package/dist/dogwood/trace.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 +12 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/integrity.json +15 -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/dwdc013.d.ts +33 -0
- package/dist/lint/post-synth/dwdc013.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 +7 -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/DWDC013.md +15 -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/okf/types/Policy.md +1 -0
- package/dist/op/activities/index.d.ts +18 -0
- package/dist/op/activities/index.d.ts.map +1 -0
- package/dist/plugin.d.ts.map +1 -1
- 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/dwdc013.ts +64 -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-dogwood.md +327 -0
- package/dist/skills/chant-cedar-meta-policy.md +119 -0
- package/package.json +7 -2
- package/src/agentcore/embed.test.ts +254 -0
- package/src/agentcore/embed.ts +399 -0
- package/src/agentcore/enforcement.test.ts +43 -0
- package/src/agentcore/enforcement.ts +92 -0
- package/src/agentcore/scan.ts +119 -0
- package/src/codegen/docs-dogwood.ts +1119 -0
- package/src/codegen/docs.ts +66 -1
- 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 +513 -0
- package/src/dogwood/cli.ts +666 -0
- package/src/dogwood/event-schema.test.ts +218 -0
- package/src/dogwood/event-schema.ts +318 -0
- package/src/dogwood/index.ts +271 -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/replay-activity.test.ts +481 -0
- package/src/dogwood/replay-activity.ts +506 -0
- package/src/dogwood/replay-op.ts +242 -0
- package/src/dogwood/scan.ts +287 -0
- package/src/dogwood/serialize.test.ts +331 -0
- package/src/dogwood/serialize.ts +246 -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/trace.test.ts +231 -0
- package/src/dogwood/trace.ts +471 -0
- package/src/dogwood/upstream.ts +41 -0
- package/src/dogwood/window.ts +124 -0
- package/src/index.ts +76 -0
- package/src/lint/audit-catalog.ts +64 -0
- package/src/lint/post-synth/dogwood-helpers.ts +139 -0
- package/src/lint/post-synth/dwd-post-synth.test.ts +374 -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/dwdc013.ts +64 -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 +14 -0
- package/src/lint/post-synth/post-synth.test.ts +7 -3
- package/src/op/activities/index.ts +27 -0
- package/src/plugin.test.ts +3 -2
- package/src/plugin.ts +30 -0
- package/src/policy-text.ts +128 -0
- package/src/serializer.ts +71 -109
- package/src/skills/chant-cedar-dogwood.md +327 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DWDC010: a temporal predicate must name an event kind the schema declares
|
|
3
|
+
*
|
|
4
|
+
* Upstream rejects this outright — a predicate naming an undeclared kind fails
|
|
5
|
+
* `dogwood validate` with code `extension`: "predicate
|
|
6
|
+
* `Drupe::Action::\"Read\"::nosuchkind` does not name a declared event (no
|
|
7
|
+
* event kind `nosuchkind` derived for action `Read`)". Catching it in
|
|
8
|
+
* TypeScript is the point of the split epic #1646 drew: everything answerable
|
|
9
|
+
* without the binary runs always and gates, and this one is answerable.
|
|
10
|
+
*
|
|
11
|
+
* The check is on the event *kind*, not the action, and that is not a
|
|
12
|
+
* shortcut. Upstream's `.dwschema` grammar declares events against a symbolic
|
|
13
|
+
* action binder — `decision event <A>::request { … }` — so the file names no
|
|
14
|
+
* actions at all and cannot be asked whether one exists. The action half of
|
|
15
|
+
* that upstream error comes from the Cedar action schema
|
|
16
|
+
* (`--policy-schema`), which is #1659's CLI-gated leg.
|
|
17
|
+
*
|
|
18
|
+
* Silent when no `.dwschema` was emitted: with none supplied,
|
|
19
|
+
* `ServiceSchema::defaults()` decides the kinds at the far end, and guessing
|
|
20
|
+
* that a project's out-of-band schema matches upstream's default would fail
|
|
21
|
+
* builds for a policy set that is fine.
|
|
22
|
+
*/
|
|
23
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
24
|
+
import { dogwoodPolicyFiles, dogwoodSchemaFiles, readEventSchema, scanPredicates } from "../../dogwood/scan";
|
|
25
|
+
|
|
26
|
+
export const dwdc010: PostSynthCheck = {
|
|
27
|
+
id: "DWDC010",
|
|
28
|
+
description: "A dogwood temporal predicate names an event kind the emitted .dwschema declares",
|
|
29
|
+
|
|
30
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
31
|
+
const schemas = dogwoodSchemaFiles(ctx);
|
|
32
|
+
if (schemas.length === 0) return [];
|
|
33
|
+
|
|
34
|
+
const declared = new Set<string>();
|
|
35
|
+
for (const schema of schemas) {
|
|
36
|
+
for (const kind of readEventSchema(schema.text).kinds) declared.add(kind);
|
|
37
|
+
}
|
|
38
|
+
if (declared.size === 0) return [];
|
|
39
|
+
|
|
40
|
+
const known = [...declared].sort().join(", ");
|
|
41
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
42
|
+
|
|
43
|
+
for (const policies of dogwoodPolicyFiles(ctx)) {
|
|
44
|
+
const seen = new Set<string>();
|
|
45
|
+
for (const ref of scanPredicates(policies.text)) {
|
|
46
|
+
if (declared.has(ref.kind)) continue;
|
|
47
|
+
const key = `${ref.action}::${ref.kind}`;
|
|
48
|
+
if (seen.has(key)) continue;
|
|
49
|
+
seen.add(key);
|
|
50
|
+
diagnostics.push({
|
|
51
|
+
checkId: "DWDC010",
|
|
52
|
+
severity: "error",
|
|
53
|
+
message: `Dogwood policy set "${policies.source}" has a temporal predicate on ${key}, but the emitted event schema declares no event kind "${ref.kind}" (declared: ${known}).`,
|
|
54
|
+
entity: policies.source,
|
|
55
|
+
lexicon: policies.lexicon,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return diagnostics;
|
|
61
|
+
},
|
|
62
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DWDC011: a temporal window must fit inside the schema's `max_window`
|
|
3
|
+
*
|
|
4
|
+
* `formerly within 48h` under a 24h cap is an upstream validation error —
|
|
5
|
+
* "temporal window `48h` exceeds the maximum allowed window `24h` set by the
|
|
6
|
+
* event schema's `max_window`" — and it is arithmetic, so it needs no binary.
|
|
7
|
+
*
|
|
8
|
+
* Unlike DWDC010 this fires with no `.dwschema` emitted, because the cap
|
|
9
|
+
* applies either way: `ServiceSchema::defaults()` caps look-back at 24h, and
|
|
10
|
+
* so does an emitted schema that omits the directive. Where several schemas
|
|
11
|
+
* are emitted the tightest cap wins — a window one consumer accepts and
|
|
12
|
+
* another rejects is still a window that will be rejected.
|
|
13
|
+
*
|
|
14
|
+
* Macro-call intervals count. `once(48h, …)` expands to `formerly within 48h`
|
|
15
|
+
* through the default library's `within ?w`, so it looks back exactly as far.
|
|
16
|
+
*/
|
|
17
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
18
|
+
import {
|
|
19
|
+
dogwoodPolicyFiles,
|
|
20
|
+
dogwoodSchemaFiles,
|
|
21
|
+
effectiveMaxWindowSeconds,
|
|
22
|
+
readEventSchema,
|
|
23
|
+
scanWindows,
|
|
24
|
+
} from "../../dogwood/scan";
|
|
25
|
+
|
|
26
|
+
export const dwdc011: PostSynthCheck = {
|
|
27
|
+
id: "DWDC011",
|
|
28
|
+
description: "A dogwood temporal window stays inside the event schema's max_window cap",
|
|
29
|
+
|
|
30
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
31
|
+
const policyFiles = dogwoodPolicyFiles(ctx);
|
|
32
|
+
if (policyFiles.length === 0) return [];
|
|
33
|
+
|
|
34
|
+
const schemas = dogwoodSchemaFiles(ctx).map((s) => readEventSchema(s.text));
|
|
35
|
+
const cap = effectiveMaxWindowSeconds(schemas);
|
|
36
|
+
const source =
|
|
37
|
+
schemas.length === 0
|
|
38
|
+
? "upstream's default cap (no event schema was emitted)"
|
|
39
|
+
: `the emitted event schema's max_window`;
|
|
40
|
+
|
|
41
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
42
|
+
|
|
43
|
+
for (const policies of policyFiles) {
|
|
44
|
+
const reported = new Set<string>();
|
|
45
|
+
for (const window of scanWindows(policies.text)) {
|
|
46
|
+
if (window.seconds <= cap.seconds) continue;
|
|
47
|
+
if (reported.has(window.text)) continue;
|
|
48
|
+
reported.add(window.text);
|
|
49
|
+
diagnostics.push({
|
|
50
|
+
checkId: "DWDC011",
|
|
51
|
+
severity: "error",
|
|
52
|
+
message: `Dogwood policy set "${policies.source}" looks back ${window.text}, past the ${cap.text} allowed by ${source}. Raise max_window in the event schema, or shorten the window.`,
|
|
53
|
+
entity: policies.source,
|
|
54
|
+
lexicon: policies.lexicon,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return diagnostics;
|
|
60
|
+
},
|
|
61
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DWDC012: `formerly`, `previous` and `since` carry a mandatory window
|
|
3
|
+
*
|
|
4
|
+
* Upstream's grammar makes `within` non-optional on all three:
|
|
5
|
+
*
|
|
6
|
+
* ```
|
|
7
|
+
* once_op = { "formerly" ~ within ~ atom }
|
|
8
|
+
* previous_op = { "previous" ~ within ~ atom }
|
|
9
|
+
* conjunct_or_since = { neg_conjunct ~ ("since" ~ within ~ atom)? }
|
|
10
|
+
* ```
|
|
11
|
+
*
|
|
12
|
+
* The typed builders make this unrepresentable — `formerly(window, body)` has
|
|
13
|
+
* nowhere to put a missing window — which is most of the fix. This check is
|
|
14
|
+
* the other part: `raw()` exists as an escape hatch, a hand-written `.dw` can
|
|
15
|
+
* be built by any other tool, and `chant audit` runs over files chant never
|
|
16
|
+
* emitted. A wall that only holds for the path that cannot breach it is not
|
|
17
|
+
* a wall.
|
|
18
|
+
*/
|
|
19
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
20
|
+
import { dogwoodPolicyFiles, scanWindowlessOperators } from "../../dogwood/scan";
|
|
21
|
+
|
|
22
|
+
export const dwdc012: PostSynthCheck = {
|
|
23
|
+
id: "DWDC012",
|
|
24
|
+
description: "A dogwood formerly/previous/since operator carries its mandatory within window",
|
|
25
|
+
|
|
26
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
27
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
28
|
+
|
|
29
|
+
for (const policies of dogwoodPolicyFiles(ctx)) {
|
|
30
|
+
const seen = new Set<string>();
|
|
31
|
+
for (const operator of scanWindowlessOperators(policies.text)) {
|
|
32
|
+
if (seen.has(operator)) continue;
|
|
33
|
+
seen.add(operator);
|
|
34
|
+
diagnostics.push({
|
|
35
|
+
checkId: "DWDC012",
|
|
36
|
+
severity: "error",
|
|
37
|
+
message: `Dogwood policy set "${policies.source}" uses \`${operator}\` with no \`within\` window. All three past-only operators require one — write \`${operator} within <n><s|m|h|d> …\`.`,
|
|
38
|
+
entity: policies.source,
|
|
39
|
+
lexicon: policies.lexicon,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return diagnostics;
|
|
45
|
+
},
|
|
46
|
+
};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DWDC013: a temporal statement embedded in AgentCore needs its event schema
|
|
3
|
+
* registered
|
|
4
|
+
*
|
|
5
|
+
* `AWS::BedrockAgentCore::Policy` carries one string. When that string is `.dw`
|
|
6
|
+
* text — `Definition.Policy.Statement`, the language-agnostic arm — the policy
|
|
7
|
+
* engine at the other end has to know what an event *is* before any temporal
|
|
8
|
+
* predicate in it can match: which kinds exist, what fields they carry, and
|
|
9
|
+
* what is pinned to the deciding request. That is the `.dwschema` half of the
|
|
10
|
+
* schema, and the resource has no property to put it in.
|
|
11
|
+
*
|
|
12
|
+
* So a build that embeds temporal text and emits no event schema has shipped
|
|
13
|
+
* half a policy. Nothing fails: the statement deploys, the predicates match
|
|
14
|
+
* nothing, and a `formerly`-guarded permit stops granting — or a
|
|
15
|
+
* `formerly`-guarded forbid stops denying, which is the direction that
|
|
16
|
+
* matters. That is the silent trap this check exists for — the emitted schema
|
|
17
|
+
* is the artifact that makes the other half reviewable, whether it is
|
|
18
|
+
* registered with the engine out of band or shipped to a `dogwood validate`
|
|
19
|
+
* run.
|
|
20
|
+
*
|
|
21
|
+
* Warning rather than error, because "not emitted here" is not the same as
|
|
22
|
+
* "does not exist": a project may register the service schema through a
|
|
23
|
+
* separate pipeline, and failing that build would be chant asserting a fact it
|
|
24
|
+
* cannot check. Emitting one with `TemporalEventSchema` silences it and makes
|
|
25
|
+
* the assumption visible in the diff.
|
|
26
|
+
*
|
|
27
|
+
* The complement of DWDC010, which asks whether the kinds a `.dw` file names
|
|
28
|
+
* are declared. This one asks the prior question, and only of statements that
|
|
29
|
+
* left the `.dw` file behind.
|
|
30
|
+
*/
|
|
31
|
+
import type { PostSynthCheck, PostSynthContext, PostSynthDiagnostic } from "@intentius/chant/lint/post-synth";
|
|
32
|
+
import { embeddedAgentCorePolicyStatements } from "../../agentcore/scan";
|
|
33
|
+
import { dogwoodSchemaFiles, temporalRegions } from "../../dogwood/scan";
|
|
34
|
+
|
|
35
|
+
export const dwdc013: PostSynthCheck = {
|
|
36
|
+
id: "DWDC013",
|
|
37
|
+
description: "An embedded AgentCore temporal statement has its event schema emitted beside it",
|
|
38
|
+
|
|
39
|
+
check(ctx: PostSynthContext): PostSynthDiagnostic[] {
|
|
40
|
+
if (dogwoodSchemaFiles(ctx).length > 0) return [];
|
|
41
|
+
|
|
42
|
+
const diagnostics: PostSynthDiagnostic[] = [];
|
|
43
|
+
const seen = new Set<string>();
|
|
44
|
+
|
|
45
|
+
for (const embedded of embeddedAgentCorePolicyStatements(ctx)) {
|
|
46
|
+
if (temporalRegions(embedded.statement).length === 0) continue;
|
|
47
|
+
|
|
48
|
+
const named = embedded.logicalId ?? embedded.source;
|
|
49
|
+
const key = `${embedded.lexicon}:${embedded.source}:${named}`;
|
|
50
|
+
if (seen.has(key)) continue;
|
|
51
|
+
seen.add(key);
|
|
52
|
+
|
|
53
|
+
diagnostics.push({
|
|
54
|
+
checkId: "DWDC013",
|
|
55
|
+
severity: "warning",
|
|
56
|
+
message: `AgentCore policy "${named}" in "${embedded.source}" carries temporal text in Definition.Policy.Statement, but the build emitted no .dwschema event schema. The policy engine needs the event kinds and their pins registered before any temporal predicate can match; without them the clause never fires and the policy silently stops doing its job. Declare a TemporalEventSchema, or record where the service schema is registered.`,
|
|
57
|
+
entity: named,
|
|
58
|
+
lexicon: embedded.lexicon,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return diagnostics;
|
|
63
|
+
},
|
|
64
|
+
};
|
|
@@ -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(7);
|
|
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
|
+
});
|