@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,242 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `PolicyReplayOp` — replay a declared `.dw` set against a recorded event
|
|
3
|
+
* trace and report divergence (#1661, epic #1646).
|
|
4
|
+
*
|
|
5
|
+
* Shape follows `WorkflowAuditOp`: an observe-dial Op whose last phase is a
|
|
6
|
+
* finding mode (`report | issue | pull-request`). What it observes is not a
|
|
7
|
+
* moving upstream but a moving *history* — a policy set that was correct
|
|
8
|
+
* against last week's traffic can decide differently against this week's, and
|
|
9
|
+
* the deterministic build cannot see that.
|
|
10
|
+
*
|
|
11
|
+
* ## Packaging: the composite ships from cedar, not from temporal
|
|
12
|
+
*
|
|
13
|
+
* `WorkflowAuditOp` and `PipelineAuditOp` live in the temporal lexicon because
|
|
14
|
+
* they hand back a `TemporalSchedule` alongside the Op, and that resource is
|
|
15
|
+
* temporal's. This composite hands back an Op and nothing else, so it has no
|
|
16
|
+
* reason to reach across — it imports `@intentius/chant/op` only, exactly as
|
|
17
|
+
* the fly lexicon's `flyDeploy` composite does. cedar therefore keeps zero
|
|
18
|
+
* runtime dependency on `@intentius/chant-lexicon-temporal`.
|
|
19
|
+
*
|
|
20
|
+
* The scheduled form is a two-line project-side pairing rather than a config
|
|
21
|
+
* flag, and that is the deliberate cost of the decision:
|
|
22
|
+
*
|
|
23
|
+
* ```ts
|
|
24
|
+
* import { TemporalSchedule } from "@intentius/chant-lexicon-temporal";
|
|
25
|
+
*
|
|
26
|
+
* export const { op } = PolicyReplayOp({ name: "policy-replay", … });
|
|
27
|
+
* export const schedule = new TemporalSchedule({
|
|
28
|
+
* scheduleId: "policy-replay-schedule",
|
|
29
|
+
* spec: { cronExpressions: ["0 6 * * *"] },
|
|
30
|
+
* action: { workflowType: "policyReplayWorkflow", taskQueue: "policy-replay" },
|
|
31
|
+
* });
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* A project that wants that already installs the temporal lexicon; a project
|
|
35
|
+
* that only wants `chant run policy-replay` on the local executor should not
|
|
36
|
+
* have to. `examples/policy-replay` is the worked recipe for both.
|
|
37
|
+
*
|
|
38
|
+
* ## Phases
|
|
39
|
+
*
|
|
40
|
+
* ```
|
|
41
|
+
* Artifacts chantBuild — emit policies.dw, the .cedarschema and the
|
|
42
|
+
* .dwschema the replay reads (skippable when they are checked in)
|
|
43
|
+
* Replay dogwoodReplay — run `dogwood replay --format json` over the
|
|
44
|
+
* bundle and the trace, write the divergence report
|
|
45
|
+
* Report dogwoodReplayReport — read that report and act on the mode
|
|
46
|
+
* ```
|
|
47
|
+
*
|
|
48
|
+
* The report file is the seam between the last two phases, the same way
|
|
49
|
+
* `dist/fly.json` is the seam between `build:fly` and `flyApply`: Op steps do
|
|
50
|
+
* not pass return values to one another, so a phase boundary needs an artifact
|
|
51
|
+
* to be a real boundary rather than a cosmetic one.
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
import { Op, activity, build, phase, type ActivityStep, type OpResource } from "@intentius/chant/op";
|
|
55
|
+
import type { PolicyReplayMode, ReplayExpectation } from "./replay-activity";
|
|
56
|
+
|
|
57
|
+
/** Where the Replay phase writes its report by default. */
|
|
58
|
+
export const DEFAULT_REPLAY_REPORT_PATH = "dist/dogwood-replay.json";
|
|
59
|
+
|
|
60
|
+
/** Options for {@link dogwoodReplayStep}. Mirrors `DogwoodReplayArgs`. */
|
|
61
|
+
export interface DogwoodReplayStepOpts {
|
|
62
|
+
policies?: string;
|
|
63
|
+
policiesPath?: string;
|
|
64
|
+
policySchema?: string;
|
|
65
|
+
policySchemaPath?: string;
|
|
66
|
+
eventSchema?: string;
|
|
67
|
+
eventSchemaPath?: string;
|
|
68
|
+
macros?: string;
|
|
69
|
+
macrosPath?: string;
|
|
70
|
+
providers?: string;
|
|
71
|
+
providersPath?: string;
|
|
72
|
+
trace?: string;
|
|
73
|
+
tracePath?: string;
|
|
74
|
+
expect?: readonly ReplayExpectation[];
|
|
75
|
+
mode?: PolicyReplayMode;
|
|
76
|
+
binary?: string;
|
|
77
|
+
cwd?: string;
|
|
78
|
+
reportPath?: string;
|
|
79
|
+
profile?: ActivityStep["profile"];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Replay a policy bundle against a trace — the typed twin of `flyApplyStep`.
|
|
84
|
+
*
|
|
85
|
+
* Resolves to the `dogwoodReplay` activity by name, which `loadActivities`
|
|
86
|
+
* binds when the project lists the `cedar` lexicon. Defaults to the
|
|
87
|
+
* `policyCheck` profile.
|
|
88
|
+
*/
|
|
89
|
+
export const dogwoodReplayStep = (opts: DogwoodReplayStepOpts): ActivityStep => {
|
|
90
|
+
const { profile, ...args } = opts;
|
|
91
|
+
return activity("dogwoodReplay", args as Record<string, unknown>, profile ?? "policyCheck");
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/** Options for {@link dogwoodReplayReportStep}. */
|
|
95
|
+
export interface DogwoodReplayReportStepOpts {
|
|
96
|
+
reportPath?: string;
|
|
97
|
+
mode?: PolicyReplayMode;
|
|
98
|
+
title?: string;
|
|
99
|
+
cwd?: string;
|
|
100
|
+
failOnDivergence?: boolean;
|
|
101
|
+
profile?: ActivityStep["profile"];
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Read a written replay report and act on its finding mode. */
|
|
105
|
+
export const dogwoodReplayReportStep = (opts: DogwoodReplayReportStepOpts = {}): ActivityStep => {
|
|
106
|
+
const { profile, ...args } = opts;
|
|
107
|
+
return activity("dogwoodReplayReport", args as Record<string, unknown>, profile ?? "fastIdempotent");
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/** Options for {@link PolicyReplayOp}. */
|
|
111
|
+
export interface PolicyReplayOpConfig {
|
|
112
|
+
/** Op name (kebab-case) — the `chant run <name>` target and the task queue base. */
|
|
113
|
+
name: string;
|
|
114
|
+
overview?: string;
|
|
115
|
+
/** Defaults to {@link name}. */
|
|
116
|
+
taskQueue?: string;
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Directory every relative path below resolves against, and the one the
|
|
120
|
+
* build script runs in. Default `.`.
|
|
121
|
+
*/
|
|
122
|
+
path?: string;
|
|
123
|
+
/**
|
|
124
|
+
* npm script that emits the `.dw` bundle. Default `build`. Pass `false` when
|
|
125
|
+
* the artifacts are checked in — the Artifacts phase is then omitted rather
|
|
126
|
+
* than run as a no-op, so the Op's phase list says what it really does.
|
|
127
|
+
*/
|
|
128
|
+
buildScript?: string | false;
|
|
129
|
+
|
|
130
|
+
/** Emitted `.dw` policy set. Default `dist/policies.dw`. */
|
|
131
|
+
policiesPath?: string;
|
|
132
|
+
/** Emitted Cedar action schema. Required by the CLI; default `dist/app.cedarschema`. */
|
|
133
|
+
policySchemaPath?: string;
|
|
134
|
+
/** Emitted `.dwschema`, when the project declares one. */
|
|
135
|
+
eventSchemaPath?: string;
|
|
136
|
+
/** Emitted macro library, when the project ships one out of line. */
|
|
137
|
+
macrosPath?: string;
|
|
138
|
+
/** `providers.json`, with the Rhai inlined under `implementation.script`. */
|
|
139
|
+
providersPath?: string;
|
|
140
|
+
|
|
141
|
+
/** The recorded trace to replay. Required — there is nothing to replay without one. */
|
|
142
|
+
tracePath: string;
|
|
143
|
+
|
|
144
|
+
/** What each decision point must decide. Empty replays and reports verdicts. */
|
|
145
|
+
expect?: readonly ReplayExpectation[];
|
|
146
|
+
|
|
147
|
+
/** What to produce on divergence. Default `report`. */
|
|
148
|
+
onFinding?: PolicyReplayMode;
|
|
149
|
+
/** Title for the issue or pull request the finding mode calls for. */
|
|
150
|
+
title?: string;
|
|
151
|
+
/**
|
|
152
|
+
* Fail the Op when the replay diverged. Default false — an observe-dial Op
|
|
153
|
+
* reports by default, and a red run is a decision the caller makes.
|
|
154
|
+
*/
|
|
155
|
+
failOnDivergence?: boolean;
|
|
156
|
+
|
|
157
|
+
/** Where the Replay phase writes its report. Default {@link DEFAULT_REPLAY_REPORT_PATH}. */
|
|
158
|
+
reportPath?: string;
|
|
159
|
+
/** Explicit `dogwood` binary path, for a runner that knows where it built one. */
|
|
160
|
+
binary?: string;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** What {@link PolicyReplayOp} hands back. */
|
|
164
|
+
export interface PolicyReplayOpResources {
|
|
165
|
+
/** The Op — discovered by `chant run <name>`, emitted by `chant build`. */
|
|
166
|
+
op: InstanceType<typeof OpResource>;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Assemble the replay Op: emit the artifacts, replay them, act on the finding.
|
|
171
|
+
*
|
|
172
|
+
* @example
|
|
173
|
+
* ```typescript
|
|
174
|
+
* export const { op } = PolicyReplayOp({
|
|
175
|
+
* name: "policy-replay",
|
|
176
|
+
* tracePath: "trace/read-after-login.log",
|
|
177
|
+
* expect: [
|
|
178
|
+
* { timestamp: 10, verdict: "allow", determiningRules: [0] },
|
|
179
|
+
* { timestamp: 7200, verdict: "deny", note: "the login is two hours stale" },
|
|
180
|
+
* ],
|
|
181
|
+
* onFinding: "issue",
|
|
182
|
+
* });
|
|
183
|
+
* ```
|
|
184
|
+
*/
|
|
185
|
+
export function PolicyReplayOp(config: PolicyReplayOpConfig): PolicyReplayOpResources {
|
|
186
|
+
const path = config.path ?? ".";
|
|
187
|
+
const mode = config.onFinding ?? "report";
|
|
188
|
+
const reportPath = config.reportPath ?? DEFAULT_REPLAY_REPORT_PATH;
|
|
189
|
+
|
|
190
|
+
const phases = [];
|
|
191
|
+
|
|
192
|
+
if (config.buildScript !== false) {
|
|
193
|
+
phases.push(phase("Artifacts", [build(path, { script: config.buildScript ?? "build" })]));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
phases.push(
|
|
197
|
+
phase("Replay", [
|
|
198
|
+
{
|
|
199
|
+
...dogwoodReplayStep({
|
|
200
|
+
cwd: path,
|
|
201
|
+
policiesPath: config.policiesPath ?? "dist/policies.dw",
|
|
202
|
+
policySchemaPath: config.policySchemaPath ?? "dist/app.cedarschema",
|
|
203
|
+
...(config.eventSchemaPath ? { eventSchemaPath: config.eventSchemaPath } : {}),
|
|
204
|
+
...(config.macrosPath ? { macrosPath: config.macrosPath } : {}),
|
|
205
|
+
...(config.providersPath ? { providersPath: config.providersPath } : {}),
|
|
206
|
+
tracePath: config.tracePath,
|
|
207
|
+
...(config.expect ? { expect: config.expect } : {}),
|
|
208
|
+
mode,
|
|
209
|
+
...(config.binary ? { binary: config.binary } : {}),
|
|
210
|
+
reportPath,
|
|
211
|
+
}),
|
|
212
|
+
// The divergence count as a workflow search attribute, so "show me the
|
|
213
|
+
// replays that found something" is one filter rather than a log read.
|
|
214
|
+
outcomeAttribute: { name: "Divergences", from: "findings" },
|
|
215
|
+
},
|
|
216
|
+
]),
|
|
217
|
+
phase("Report", [
|
|
218
|
+
dogwoodReplayReportStep({
|
|
219
|
+
cwd: path,
|
|
220
|
+
reportPath,
|
|
221
|
+
mode,
|
|
222
|
+
...(config.title ? { title: config.title } : {}),
|
|
223
|
+
...(config.failOnDivergence ? { failOnDivergence: true } : {}),
|
|
224
|
+
}),
|
|
225
|
+
]),
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
return {
|
|
229
|
+
op: Op({
|
|
230
|
+
name: config.name,
|
|
231
|
+
overview:
|
|
232
|
+
config.overview ??
|
|
233
|
+
"Replay the declared dogwood policy set against a recorded event trace and report divergence",
|
|
234
|
+
taskQueue: config.taskQueue ?? config.name,
|
|
235
|
+
searchAttributes: {
|
|
236
|
+
Audit: "true",
|
|
237
|
+
Surface: "cedar-dogwood",
|
|
238
|
+
},
|
|
239
|
+
phases,
|
|
240
|
+
}),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading the emitted dogwood artifacts back, for the DWD post-synth checks.
|
|
3
|
+
*
|
|
4
|
+
* The checks judge the *text*, not the in-memory model, for the same reason
|
|
5
|
+
* the CED checks judge `policies.cedar.json`: `chant audit` runs over a
|
|
6
|
+
* checked-in artifact chant did not write, and a wall that only fires on
|
|
7
|
+
* chant's own output is not a wall. It also means the typed builders and the
|
|
8
|
+
* walls are independent — DWDC012 catches a windowless `formerly` even though
|
|
9
|
+
* the builders cannot construct one, because `raw()` and a hand-written `.dw`
|
|
10
|
+
* both can.
|
|
11
|
+
*
|
|
12
|
+
* This is scanning, not parsing. Upstream owns the parser and #1659 owns
|
|
13
|
+
* shelling to it; what is here is the subset of the surface that answers the
|
|
14
|
+
* three questions the walls ask, plus enough comment handling not to be fooled
|
|
15
|
+
* by a commented-out clause.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { PostSynthContext } from "@intentius/chant/lint/post-synth";
|
|
19
|
+
import { DEFAULT_MAX_WINDOW, windowSeconds, type TimeUnit } from "./window";
|
|
20
|
+
import { DOGWOOD_EVENT_SCHEMA_SUFFIX, DOGWOOD_POLICY_SUFFIX } from "./serialize";
|
|
21
|
+
|
|
22
|
+
/** One emitted artifact, with everything needed to name it in a finding. */
|
|
23
|
+
export interface DogwoodArtifact {
|
|
24
|
+
/** The lexicon output it came from. */
|
|
25
|
+
lexicon: string;
|
|
26
|
+
/** The filename, which is what a finding points the reader at. */
|
|
27
|
+
source: string;
|
|
28
|
+
/** The file's text. */
|
|
29
|
+
text: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Every `.dw` policy file in the build output. */
|
|
33
|
+
export function dogwoodPolicyFiles(ctx: PostSynthContext): DogwoodArtifact[] {
|
|
34
|
+
return filesWithSuffix(ctx, DOGWOOD_POLICY_SUFFIX);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Every `.dwschema` event-schema file in the build output. */
|
|
38
|
+
export function dogwoodSchemaFiles(ctx: PostSynthContext): DogwoodArtifact[] {
|
|
39
|
+
return filesWithSuffix(ctx, DOGWOOD_EVENT_SCHEMA_SUFFIX);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function filesWithSuffix(ctx: PostSynthContext, suffix: string): DogwoodArtifact[] {
|
|
43
|
+
const found: DogwoodArtifact[] = [];
|
|
44
|
+
for (const [lexicon, output] of ctx.outputs) {
|
|
45
|
+
if (typeof output === "string") continue;
|
|
46
|
+
for (const [filename, content] of Object.entries(output.files ?? {})) {
|
|
47
|
+
if (typeof content !== "string") continue;
|
|
48
|
+
if (!filename.endsWith(suffix)) continue;
|
|
49
|
+
found.push({ lexicon, source: filename, text: content });
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return found;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── Comments ──────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Blank out `//` comments, leaving string literals intact.
|
|
59
|
+
*
|
|
60
|
+
* String literals stay because a predicate's action id lives inside one
|
|
61
|
+
* (`Drupe::Action::"Read"`), and blanking those would blind every scan below.
|
|
62
|
+
* The walk is string-aware in the other direction too: a `//` inside a quoted
|
|
63
|
+
* value (a URL, say) is not a comment.
|
|
64
|
+
*
|
|
65
|
+
* Comment bytes become spaces so every offset in the result still lines up
|
|
66
|
+
* with the original file.
|
|
67
|
+
*/
|
|
68
|
+
export function blankComments(text: string): string {
|
|
69
|
+
const out = text.split("");
|
|
70
|
+
let inString = false;
|
|
71
|
+
for (let i = 0; i < text.length; i++) {
|
|
72
|
+
const ch = text[i];
|
|
73
|
+
if (inString) {
|
|
74
|
+
if (ch === "\\") {
|
|
75
|
+
i++;
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (ch === '"') inString = false;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (ch === '"') {
|
|
82
|
+
inString = true;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (ch === "/" && text[i + 1] === "/") {
|
|
86
|
+
while (i < text.length && text[i] !== "\n") {
|
|
87
|
+
out[i] = " ";
|
|
88
|
+
i++;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return out.join("");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ── Temporal regions ──────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The stretches of a `.dw` file that the temporal sub-parser reads: every
|
|
99
|
+
* `temporal { … }` marker body, and every `def temporal … { … }` macro body.
|
|
100
|
+
*
|
|
101
|
+
* Scanning only these is what keeps a Cedar `when { … }` clause from being
|
|
102
|
+
* mistaken for temporal source — `context.retryWindow == 3` should not read as
|
|
103
|
+
* a window, and a Cedar attribute named `since` is not the `since` operator.
|
|
104
|
+
*/
|
|
105
|
+
export function temporalRegions(text: string): string[] {
|
|
106
|
+
const source = blankComments(text);
|
|
107
|
+
const regions: string[] = [];
|
|
108
|
+
|
|
109
|
+
const markers = /\btemporal\s*\{/g;
|
|
110
|
+
for (let m = markers.exec(source); m !== null; m = markers.exec(source)) {
|
|
111
|
+
const body = matchedBlock(source, m.index + m[0].length - 1);
|
|
112
|
+
if (body !== undefined) regions.push(body);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const macros = /\bdef\s+temporal\s+[A-Za-z_][A-Za-z0-9_]*\s*\([^)]*\)\s*\{/g;
|
|
116
|
+
for (let m = macros.exec(source); m !== null; m = macros.exec(source)) {
|
|
117
|
+
const body = matchedBlock(source, m.index + m[0].length - 1);
|
|
118
|
+
if (body !== undefined) regions.push(body);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return regions;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** The text between `text[open]` (a `{`) and its matching `}`, or undefined. */
|
|
125
|
+
function matchedBlock(text: string, open: number): string | undefined {
|
|
126
|
+
let depth = 0;
|
|
127
|
+
let inString = false;
|
|
128
|
+
for (let i = open; i < text.length; i++) {
|
|
129
|
+
const ch = text[i];
|
|
130
|
+
if (inString) {
|
|
131
|
+
if (ch === "\\") i++;
|
|
132
|
+
else if (ch === '"') inString = false;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
if (ch === '"') inString = true;
|
|
136
|
+
else if (ch === "{") depth++;
|
|
137
|
+
else if (ch === "}") {
|
|
138
|
+
depth--;
|
|
139
|
+
if (depth === 0) return text.slice(open + 1, i);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── What the walls ask ────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
/** One `Ns::Action::"Name"::kind` predicate head found in temporal source. */
|
|
148
|
+
export interface TemporalPredicateRef {
|
|
149
|
+
/** The qualified action, quoted id and all. */
|
|
150
|
+
action: string;
|
|
151
|
+
/** The event kind segment after the quoted id. */
|
|
152
|
+
kind: string;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const PREDICATE = /([A-Za-z_][A-Za-z0-9_]*(?:::[A-Za-z_][A-Za-z0-9_]*)*::"[^"]*")::([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
156
|
+
|
|
157
|
+
/** Every temporal predicate head in a `.dw` file, in source order. */
|
|
158
|
+
export function scanPredicates(text: string): TemporalPredicateRef[] {
|
|
159
|
+
const refs: TemporalPredicateRef[] = [];
|
|
160
|
+
for (const region of temporalRegions(text)) {
|
|
161
|
+
for (let m = PREDICATE.exec(region); m !== null; m = PREDICATE.exec(region)) {
|
|
162
|
+
refs.push({ action: m[1], kind: m[2] });
|
|
163
|
+
}
|
|
164
|
+
PREDICATE.lastIndex = 0;
|
|
165
|
+
}
|
|
166
|
+
return refs;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** One window literal found in temporal source. */
|
|
170
|
+
export interface WindowRef {
|
|
171
|
+
/** As written: `48h`. */
|
|
172
|
+
text: string;
|
|
173
|
+
seconds: number;
|
|
174
|
+
/** `within` for an operator's window, `argument` for a bare macro-call interval. */
|
|
175
|
+
position: "within" | "argument";
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const WITHIN_WINDOW = /\bwithin\s+(\d+)([smhd])\b/g;
|
|
179
|
+
/** A bare interval in macro-call argument position: `once(1h, …)`, `f(a, 30m)`. */
|
|
180
|
+
const ARGUMENT_WINDOW = /[(,]\s*(\d+)([smhd])\s*(?=[,)])/g;
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Every window literal in a `.dw` file's temporal source.
|
|
184
|
+
*
|
|
185
|
+
* Both shapes count against `max_window`: a macro-call interval is the window
|
|
186
|
+
* a `within ?w` in the macro body resolves to, so `once(48h, …)` looks back
|
|
187
|
+
* exactly as far as `formerly within 48h` does.
|
|
188
|
+
*/
|
|
189
|
+
export function scanWindows(text: string): WindowRef[] {
|
|
190
|
+
const windows: WindowRef[] = [];
|
|
191
|
+
for (const region of temporalRegions(text)) {
|
|
192
|
+
for (let m = WITHIN_WINDOW.exec(region); m !== null; m = WITHIN_WINDOW.exec(region)) {
|
|
193
|
+
windows.push(windowRef(m[1], m[2], "within"));
|
|
194
|
+
}
|
|
195
|
+
WITHIN_WINDOW.lastIndex = 0;
|
|
196
|
+
for (let m = ARGUMENT_WINDOW.exec(region); m !== null; m = ARGUMENT_WINDOW.exec(region)) {
|
|
197
|
+
windows.push(windowRef(m[1], m[2], "argument"));
|
|
198
|
+
}
|
|
199
|
+
ARGUMENT_WINDOW.lastIndex = 0;
|
|
200
|
+
}
|
|
201
|
+
return windows;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function windowRef(value: string, unit: string, position: WindowRef["position"]): WindowRef {
|
|
205
|
+
const w = { value: Number(value), unit: unit as TimeUnit };
|
|
206
|
+
return { text: `${w.value}${w.unit}`, seconds: windowSeconds(w), position };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Every `formerly` / `previous` / `since` that is not followed by a `within`.
|
|
211
|
+
*
|
|
212
|
+
* Unrepresentable through the builders — {@link formerly} and its siblings all
|
|
213
|
+
* take the window as an argument — but reachable through `raw()`, through a
|
|
214
|
+
* hand-written `.dw`, and through an audit of a file chant never wrote.
|
|
215
|
+
*/
|
|
216
|
+
export function scanWindowlessOperators(text: string): string[] {
|
|
217
|
+
const found: string[] = [];
|
|
218
|
+
const pattern = /\b(formerly|previous|since)\b(?!\s+within\b)/g;
|
|
219
|
+
for (const region of temporalRegions(text)) {
|
|
220
|
+
for (let m = pattern.exec(region); m !== null; m = pattern.exec(region)) {
|
|
221
|
+
found.push(m[1]);
|
|
222
|
+
}
|
|
223
|
+
pattern.lastIndex = 0;
|
|
224
|
+
}
|
|
225
|
+
return found;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ── The event schema side ─────────────────────────────────────────
|
|
229
|
+
|
|
230
|
+
/** What a `.dwschema` file says, as far as the walls need to know. */
|
|
231
|
+
export interface EventSchemaFacts {
|
|
232
|
+
/** Every declared event kind. */
|
|
233
|
+
kinds: string[];
|
|
234
|
+
/** The `max_window` directive in seconds, or upstream's 24h default when absent. */
|
|
235
|
+
maxWindowSeconds: number;
|
|
236
|
+
/** As written (`30d`), or undefined when the directive is absent. */
|
|
237
|
+
maxWindowText?: string;
|
|
238
|
+
/** True when at least one field carries a `pin … = …`. */
|
|
239
|
+
hasPin: boolean;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const EVENT_DECL = /\bevent\s*<\s*[A-Za-z_][A-Za-z0-9_]*\s*>\s*::\s*([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
243
|
+
const MAX_WINDOW = /\bmax_window\s*=\s*(\d+)([smhd])\b/;
|
|
244
|
+
const PINNED_FIELD = /\bpin\s+[A-Za-z_][A-Za-z0-9_]*\s*:/;
|
|
245
|
+
|
|
246
|
+
/** Read a `.dwschema` file's declarations. */
|
|
247
|
+
export function readEventSchema(text: string): EventSchemaFacts {
|
|
248
|
+
const source = blankComments(text);
|
|
249
|
+
|
|
250
|
+
const kinds: string[] = [];
|
|
251
|
+
for (let m = EVENT_DECL.exec(source); m !== null; m = EVENT_DECL.exec(source)) {
|
|
252
|
+
if (!kinds.includes(m[1])) kinds.push(m[1]);
|
|
253
|
+
}
|
|
254
|
+
EVENT_DECL.lastIndex = 0;
|
|
255
|
+
|
|
256
|
+
const max = MAX_WINDOW.exec(source);
|
|
257
|
+
return {
|
|
258
|
+
kinds,
|
|
259
|
+
maxWindowSeconds: max
|
|
260
|
+
? windowSeconds({ value: Number(max[1]), unit: max[2] as TimeUnit })
|
|
261
|
+
: windowSeconds(DEFAULT_MAX_WINDOW),
|
|
262
|
+
maxWindowText: max ? `${max[1]}${max[2]}` : undefined,
|
|
263
|
+
hasPin: PINNED_FIELD.test(source),
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* The look-back cap in force for a build.
|
|
269
|
+
*
|
|
270
|
+
* With no emitted schema, `ServiceSchema::defaults()` applies and the cap is
|
|
271
|
+
* 24h. With several, the tightest one wins — a window legal under one schema
|
|
272
|
+
* and not another is a window some consumer will reject.
|
|
273
|
+
*/
|
|
274
|
+
export function effectiveMaxWindowSeconds(schemas: EventSchemaFacts[]): { seconds: number; text: string } {
|
|
275
|
+
if (schemas.length === 0) {
|
|
276
|
+
return {
|
|
277
|
+
seconds: windowSeconds(DEFAULT_MAX_WINDOW),
|
|
278
|
+
text: `${DEFAULT_MAX_WINDOW.value}${DEFAULT_MAX_WINDOW.unit}`,
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
let tightest = schemas[0];
|
|
282
|
+
for (const s of schemas) if (s.maxWindowSeconds < tightest.maxWindowSeconds) tightest = s;
|
|
283
|
+
return {
|
|
284
|
+
seconds: tightest.maxWindowSeconds,
|
|
285
|
+
text: tightest.maxWindowText ?? `${DEFAULT_MAX_WINDOW.value}${DEFAULT_MAX_WINDOW.unit}`,
|
|
286
|
+
};
|
|
287
|
+
}
|