@intentius/chant-lexicon-cedar 0.124.31 → 0.124.33
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/dist/agentcore/embed.js +250 -0
- package/dist/agentcore/enforcement.js +81 -0
- package/dist/agentcore/scan.js +93 -0
- package/dist/avp/ambient.js +100 -0
- package/dist/avp/client.js +318 -0
- package/dist/avp/describe-resources.js +144 -0
- package/dist/avp/embed.js +125 -0
- package/dist/avp/live-export.js +151 -0
- package/dist/avp/ownership.js +144 -0
- package/dist/avp/statement.js +45 -0
- package/dist/avp/store.js +105 -0
- package/dist/avp/testdata/mock-transport.js +100 -0
- package/dist/codegen/docs-cli.js +6 -0
- package/dist/codegen/docs.js +133 -0
- package/dist/codegen/emit.js +331 -0
- package/dist/codegen/generate-cli.js +18 -0
- package/dist/codegen/generate.js +122 -0
- package/dist/codegen/naming.js +90 -0
- package/dist/codegen/package.js +33 -0
- package/dist/commands.js +86 -0
- package/dist/composites/deny-by-default-set.js +51 -0
- package/dist/composites/index.js +9 -0
- package/dist/composites/owner-can-manage.js +48 -0
- package/dist/config.js +159 -0
- package/dist/coverage.js +112 -0
- package/dist/detect.js +61 -0
- package/dist/dogwood/cli.js +477 -0
- package/dist/dogwood/event-schema.js +219 -0
- package/dist/dogwood/index.js +32 -0
- package/dist/dogwood/macros.js +175 -0
- package/dist/dogwood/policy.js +58 -0
- package/dist/dogwood/replay-activity.js +313 -0
- package/dist/dogwood/replay-op.js +134 -0
- package/dist/dogwood/scan.js +234 -0
- package/dist/dogwood/serialize.js +199 -0
- package/dist/dogwood/temporal.js +354 -0
- package/dist/dogwood/trace.js +269 -0
- package/dist/dogwood/upstream.js +40 -0
- package/dist/dogwood/window.js +82 -0
- package/dist/gate-policy-pack.js +189 -0
- package/dist/gate-policy.js +168 -0
- package/dist/gate-schema.js +122 -0
- package/dist/generated/index.js +94 -0
- package/dist/generated/runtime.js +2 -0
- package/dist/import/adapter.js +54 -0
- package/dist/import/clause-text.js +166 -0
- package/dist/import/generator.js +178 -0
- package/dist/import/parser.js +273 -0
- package/dist/index.js +79 -0
- package/dist/init-templates.js +330 -0
- package/dist/integrity.json +2 -2
- package/dist/lint/audit-catalog.js +65 -0
- package/dist/lint/audit-lineage.js +23 -0
- package/dist/lint/post-synth/cedar-helpers.js +123 -0
- package/dist/lint/post-synth/cedc010.js +59 -0
- package/dist/lint/post-synth/cedc011.js +29 -0
- package/dist/lint/post-synth/cedc012.js +34 -0
- package/dist/lint/post-synth/cedc013.js +45 -0
- package/dist/lint/post-synth/cedc014.js +54 -0
- package/dist/lint/post-synth/cede010.js +51 -0
- package/dist/lint/post-synth/cede011.js +36 -0
- package/dist/lint/post-synth/ceds010.js +31 -0
- package/dist/lint/post-synth/ceds011.js +22 -0
- package/dist/lint/post-synth/ceds012.js +31 -0
- package/dist/lint/post-synth/dogwood-helpers.js +94 -0
- package/dist/lint/post-synth/dwdc010.js +38 -0
- package/dist/lint/post-synth/dwdc011.js +34 -0
- package/dist/lint/post-synth/dwdc012.js +24 -0
- package/dist/lint/post-synth/dwdc013.js +29 -0
- package/dist/lint/post-synth/dwde010.js +84 -0
- package/dist/lint/post-synth/dwde011.js +67 -0
- package/dist/lint/post-synth/dwds010.js +21 -0
- package/dist/lint/post-synth/index.js +36 -0
- package/dist/lint/post-synth/wasm-helpers.js +234 -0
- package/dist/lint/rules/index.js +9 -0
- package/dist/lint/rules/policy-shape.js +114 -0
- package/dist/lsp/completions.js +52 -0
- package/dist/lsp/hover.js +56 -0
- package/dist/lsp/registry.d.ts.map +1 -1
- package/dist/lsp/registry.js +51 -0
- package/dist/manifest.json +1 -1
- package/dist/mcp/index.js +82 -0
- package/dist/mcp/policy-coverage.js +134 -0
- package/dist/op/activities/index.js +16 -0
- package/dist/package-cli.js +18 -0
- package/dist/plugin.js +304 -0
- package/dist/policy-text.js +117 -0
- package/dist/schema-artifact.js +66 -0
- package/dist/serializer.js +203 -0
- package/dist/spec/fetch.js +96 -0
- package/dist/spec/parse.js +164 -0
- package/dist/spec/pin.js +170 -0
- package/dist/spec/wasm.js +194 -0
- package/dist/validate-cli.js +7 -0
- package/dist/validate.js +74 -0
- package/package.json +7 -7
- package/src/codegen/package.ts +2 -2
- package/src/coverage.ts +2 -2
- package/src/import/generator.ts +2 -2
- package/src/lsp/registry.ts +2 -1
- package/src/spec/fetch.ts +2 -2
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { DOGWOOD_BINARY_NAME, DOGWOOD_SEARCH_ORDER, findDogwoodBinary, formatDogwoodDiagnostic, runDogwoodValidate, } from "../../dogwood/cli.js";
|
|
2
|
+
import { describeBundle, planDogwoodRuns } from "./dogwood-helpers.js";
|
|
3
|
+
export const dwde010 = {
|
|
4
|
+
id: "DWDE010",
|
|
5
|
+
description: "Emitted dogwood policy sets validate clean under `dogwood validate`, when the binary is available",
|
|
6
|
+
check(ctx) {
|
|
7
|
+
const plan = planDogwoodRuns(ctx);
|
|
8
|
+
if (!plan.hasPolicies)
|
|
9
|
+
return [];
|
|
10
|
+
const binary = findDogwoodBinary();
|
|
11
|
+
if (!binary) {
|
|
12
|
+
return [
|
|
13
|
+
{
|
|
14
|
+
checkId: "DWDE010",
|
|
15
|
+
severity: "info",
|
|
16
|
+
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.`,
|
|
17
|
+
lexicon: plan.lexicon,
|
|
18
|
+
},
|
|
19
|
+
];
|
|
20
|
+
}
|
|
21
|
+
if (plan.blocked) {
|
|
22
|
+
return [
|
|
23
|
+
{
|
|
24
|
+
checkId: "DWDE010",
|
|
25
|
+
severity: "info",
|
|
26
|
+
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.`,
|
|
27
|
+
lexicon: plan.lexicon,
|
|
28
|
+
},
|
|
29
|
+
];
|
|
30
|
+
}
|
|
31
|
+
const diagnostics = [];
|
|
32
|
+
for (const prepared of plan.bundles) {
|
|
33
|
+
const where = describeBundle(prepared);
|
|
34
|
+
const result = runDogwoodValidate(binary.path, prepared.bundle);
|
|
35
|
+
if (result.kind === "unusable") {
|
|
36
|
+
diagnostics.push({
|
|
37
|
+
checkId: "DWDE010",
|
|
38
|
+
severity: "warning",
|
|
39
|
+
message: `Dogwood policy set ${where} could not be validated — ${result.reason}. The policy set was neither accepted nor rejected.`,
|
|
40
|
+
entity: prepared.source,
|
|
41
|
+
lexicon: prepared.lexicon,
|
|
42
|
+
});
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (result.kind === "fatal") {
|
|
46
|
+
for (const finding of [result.error, ...result.related]) {
|
|
47
|
+
diagnostics.push({
|
|
48
|
+
checkId: "DWDE010",
|
|
49
|
+
severity: "error",
|
|
50
|
+
message: `Dogwood policy set ${where} could not be parsed or lowered: ${formatDogwoodDiagnostic(finding)}`,
|
|
51
|
+
entity: prepared.source,
|
|
52
|
+
lexicon: prepared.lexicon,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (result.kind === "rejected") {
|
|
58
|
+
for (const finding of result.errors) {
|
|
59
|
+
diagnostics.push({
|
|
60
|
+
checkId: "DWDE010",
|
|
61
|
+
severity: "error",
|
|
62
|
+
message: `Dogwood policy set ${where} fails \`dogwood validate\`: ${formatDogwoodDiagnostic(finding)}`,
|
|
63
|
+
entity: prepared.source,
|
|
64
|
+
lexicon: prepared.lexicon,
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Upstream's own warnings ride along at warning severity. They do not
|
|
69
|
+
// fail `dogwood validate` (`passed` ignores them), and they should not
|
|
70
|
+
// fail a build either — but dropping them would throw away the half of
|
|
71
|
+
// the report that says a policy is legal and pointless.
|
|
72
|
+
for (const finding of result.warnings) {
|
|
73
|
+
diagnostics.push({
|
|
74
|
+
checkId: "DWDE010",
|
|
75
|
+
severity: "warning",
|
|
76
|
+
message: `Dogwood policy set ${where} draws a \`dogwood validate\` warning: ${formatDogwoodDiagnostic(finding)}`,
|
|
77
|
+
entity: prepared.source,
|
|
78
|
+
lexicon: prepared.lexicon,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return diagnostics;
|
|
83
|
+
},
|
|
84
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { findDogwoodBinary, runDogwoodLower } from "../../dogwood/cli.js";
|
|
2
|
+
import { describeBundle, planDogwoodRuns } from "./dogwood-helpers.js";
|
|
3
|
+
import { loadWasm, validatePolicySet } from "./wasm-helpers.js";
|
|
4
|
+
export const dwde011 = {
|
|
5
|
+
id: "DWDE011",
|
|
6
|
+
description: "The Cedar `dogwood lower` produces validates clean against the augmented schema (cedar-wasm validate)",
|
|
7
|
+
check(ctx) {
|
|
8
|
+
const plan = planDogwoodRuns(ctx);
|
|
9
|
+
if (!plan.hasPolicies || plan.blocked || plan.bundles.length === 0)
|
|
10
|
+
return [];
|
|
11
|
+
const binary = findDogwoodBinary();
|
|
12
|
+
if (!binary)
|
|
13
|
+
return [];
|
|
14
|
+
// A missing validator is not this check's finding to report; the cedar leg
|
|
15
|
+
// says so already through CEDC010.
|
|
16
|
+
const wasm = loadWasm();
|
|
17
|
+
if (!wasm)
|
|
18
|
+
return [];
|
|
19
|
+
const diagnostics = [];
|
|
20
|
+
for (const prepared of plan.bundles) {
|
|
21
|
+
const where = describeBundle(prepared);
|
|
22
|
+
const lowered = runDogwoodLower(binary.path, prepared.bundle);
|
|
23
|
+
if (lowered.kind === "unusable") {
|
|
24
|
+
diagnostics.push({
|
|
25
|
+
checkId: "DWDE011",
|
|
26
|
+
severity: "warning",
|
|
27
|
+
message: `Dogwood policy set ${where} could not be lowered to Cedar — ${lowered.reason}. Its non-temporal body was not checked by Cedar's validator.`,
|
|
28
|
+
entity: prepared.source,
|
|
29
|
+
lexicon: prepared.lexicon,
|
|
30
|
+
});
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (lowered.kind === "fatal") {
|
|
34
|
+
// The same fatal DWDE010 reports from its own run. Saying it twice
|
|
35
|
+
// helps nobody, so this arm stays quiet and lets the validate leg own
|
|
36
|
+
// the parse/lower channel.
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
const { cedarPolicies, cedarSchema } = lowered.value;
|
|
40
|
+
// The lowered policies go in as Cedar text. cedar-wasm synthesizes ids
|
|
41
|
+
// (`policy0`, `policy1`) for a bare-string set and does not read `@id`
|
|
42
|
+
// annotations as ids (#1648), so a finding names the .dw file it came
|
|
43
|
+
// from as well as the id the validator used.
|
|
44
|
+
const outcome = validatePolicySet(wasm, { staticPolicies: cedarPolicies }, cedarSchema);
|
|
45
|
+
if (outcome.failure) {
|
|
46
|
+
diagnostics.push({
|
|
47
|
+
checkId: "DWDE011",
|
|
48
|
+
severity: "error",
|
|
49
|
+
message: `The Cedar lowered from dogwood policy set ${where} could not be validated: ${outcome.failure}`,
|
|
50
|
+
entity: prepared.source,
|
|
51
|
+
lexicon: prepared.lexicon,
|
|
52
|
+
});
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
for (const finding of outcome.errors) {
|
|
56
|
+
diagnostics.push({
|
|
57
|
+
checkId: "DWDE011",
|
|
58
|
+
severity: "error",
|
|
59
|
+
message: `The Cedar lowered from dogwood policy set ${where} fails Cedar validation (lowered policy "${finding.policyId}"): ${finding.message}`,
|
|
60
|
+
entity: prepared.source,
|
|
61
|
+
lexicon: prepared.lexicon,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
return diagnostics;
|
|
66
|
+
},
|
|
67
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { dogwoodSchemaFiles, readEventSchema } from "../../dogwood/scan.js";
|
|
2
|
+
export const dwds010 = {
|
|
3
|
+
id: "DWDS010",
|
|
4
|
+
description: "An emitted dogwood event schema pins its temporal predicates to a request-side value",
|
|
5
|
+
check(ctx) {
|
|
6
|
+
const diagnostics = [];
|
|
7
|
+
for (const schema of dogwoodSchemaFiles(ctx)) {
|
|
8
|
+
const facts = readEventSchema(schema.text);
|
|
9
|
+
if (facts.kinds.length === 0 || facts.hasPin)
|
|
10
|
+
continue;
|
|
11
|
+
diagnostics.push({
|
|
12
|
+
checkId: "DWDS010",
|
|
13
|
+
severity: "warning",
|
|
14
|
+
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.`,
|
|
15
|
+
entity: schema.source,
|
|
16
|
+
lexicon: schema.lexicon,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
return diagnostics;
|
|
20
|
+
},
|
|
21
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { cedc010 } from "./cedc010.js";
|
|
2
|
+
import { cedc011 } from "./cedc011.js";
|
|
3
|
+
import { cedc012 } from "./cedc012.js";
|
|
4
|
+
import { cedc013 } from "./cedc013.js";
|
|
5
|
+
import { cedc014 } from "./cedc014.js";
|
|
6
|
+
import { cede010 } from "./cede010.js";
|
|
7
|
+
import { cede011 } from "./cede011.js";
|
|
8
|
+
import { ceds010 } from "./ceds010.js";
|
|
9
|
+
import { ceds011 } from "./ceds011.js";
|
|
10
|
+
import { ceds012 } from "./ceds012.js";
|
|
11
|
+
import { dwdc010 } from "./dwdc010.js";
|
|
12
|
+
import { dwdc011 } from "./dwdc011.js";
|
|
13
|
+
import { dwdc012 } from "./dwdc012.js";
|
|
14
|
+
import { dwdc013 } from "./dwdc013.js";
|
|
15
|
+
import { dwde010 } from "./dwde010.js";
|
|
16
|
+
import { dwde011 } from "./dwde011.js";
|
|
17
|
+
import { dwds010 } from "./dwds010.js";
|
|
18
|
+
export const postSynthChecks = [
|
|
19
|
+
cedc010,
|
|
20
|
+
cedc011,
|
|
21
|
+
cedc012,
|
|
22
|
+
cedc013,
|
|
23
|
+
cedc014,
|
|
24
|
+
cede010,
|
|
25
|
+
cede011,
|
|
26
|
+
ceds010,
|
|
27
|
+
ceds011,
|
|
28
|
+
ceds012,
|
|
29
|
+
dwdc010,
|
|
30
|
+
dwdc011,
|
|
31
|
+
dwdc012,
|
|
32
|
+
dwdc013,
|
|
33
|
+
dwde010,
|
|
34
|
+
dwde011,
|
|
35
|
+
dwds010,
|
|
36
|
+
];
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `cedar-wasm` seam for the cedar post-synth checks.
|
|
3
|
+
*
|
|
4
|
+
* Cedar's real parser and validator ship as `@cedar-policy/cedar-wasm`, which
|
|
5
|
+
* runs in-process — no CLI on PATH, no Docker. Everything the checks need from
|
|
6
|
+
* it goes through this module so the traps live in one place. The traps, all
|
|
7
|
+
* of them empirically verified against 4.12.0 (chant #1648):
|
|
8
|
+
*
|
|
9
|
+
* 1. **`type: "success"` does not mean valid.** `failure` means the *call*
|
|
10
|
+
* failed; `success` means validation *ran*, and can carry any number of
|
|
11
|
+
* `validationErrors`. {@link validatePolicySet} asserts both.
|
|
12
|
+
* 2. **`validationErrors` ordering is non-deterministic** — the validator
|
|
13
|
+
* parallelizes across policies and never re-sorts. Forty identical calls
|
|
14
|
+
* produced six distinct orderings. Everything this module returns is sorted
|
|
15
|
+
* by `policyId` then `message`, so a check's output is stable.
|
|
16
|
+
* 3. **Malformed input throws instead of returning a failure answer.** Missing
|
|
17
|
+
* fields on a call struct raise a bare serde `Error` whose `line 1 column N`
|
|
18
|
+
* refers to the internal call struct, not to anything the user wrote — it is
|
|
19
|
+
* meaningless to surface. {@link call} wraps every entry point and normalizes
|
|
20
|
+
* both channels into one result type.
|
|
21
|
+
* 4. **A bare-string policy set gets synthesized ids** (`policy0`, `policy1`),
|
|
22
|
+
* and `@id` annotations do *not* become the policy id. Everything here
|
|
23
|
+
* passes `staticPolicies` as a `Record<PolicyId, Policy>` keyed by chant's
|
|
24
|
+
* own ids, so a diagnostic names something a reader can find.
|
|
25
|
+
* 5. **Only `strict` validation mode exists** in this build; any other value
|
|
26
|
+
* throws. The settings are left at their default rather than passed.
|
|
27
|
+
*
|
|
28
|
+
* Only the `/nodejs` flavor is imported. The root/`esm` flavor imports the
|
|
29
|
+
* `.wasm` as an ES module, which Bun cannot start (`__wbindgen_start is not a
|
|
30
|
+
* function`) and Node only supports behind an experimental warning printed on
|
|
31
|
+
* every run; `/web` needs an explicit `initSync`. `/nodejs` is the one flavor
|
|
32
|
+
* that loads under `node`, `tsx`, `vitest`, and `bun` alike.
|
|
33
|
+
*
|
|
34
|
+
* Excluded from check auto-discovery by the "helper" filename filter.
|
|
35
|
+
*/
|
|
36
|
+
import { createRequire } from "module";
|
|
37
|
+
import { isRecord, } from "./cedar-helpers.js";
|
|
38
|
+
let cached;
|
|
39
|
+
let loadError;
|
|
40
|
+
/**
|
|
41
|
+
* The wasm module, loaded on first use.
|
|
42
|
+
*
|
|
43
|
+
* Lazy and `require`-based on purpose. The post-synth barrel is imported
|
|
44
|
+
* statically by `plugin.ts`, and every `chant lint`/`chant audit` run loads
|
|
45
|
+
* every active lexicon's plugin — a top-level import would pull 4 MB of wasm
|
|
46
|
+
* into any build that merely has cedar installed. `check()` is synchronous, so
|
|
47
|
+
* `await import()` is not available; `createRequire` is. The `/nodejs` flavor
|
|
48
|
+
* is CommonJS with a synchronous `readFileSync` init, so there is nothing to
|
|
49
|
+
* await anyway.
|
|
50
|
+
*/
|
|
51
|
+
export function loadWasm() {
|
|
52
|
+
if (cached || loadError)
|
|
53
|
+
return cached;
|
|
54
|
+
try {
|
|
55
|
+
const require = createRequire(import.meta.url);
|
|
56
|
+
cached = require("@cedar-policy/cedar-wasm/nodejs");
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
loadError = err instanceof Error ? err.message : String(err);
|
|
60
|
+
}
|
|
61
|
+
return cached;
|
|
62
|
+
}
|
|
63
|
+
/** Why the wasm could not be loaded, if it could not be. */
|
|
64
|
+
export function wasmLoadError() {
|
|
65
|
+
loadWasm();
|
|
66
|
+
return loadError;
|
|
67
|
+
}
|
|
68
|
+
/** Reset the module cache. Test-only — lets a test exercise the load-failure path. */
|
|
69
|
+
export function resetWasmCache() {
|
|
70
|
+
cached = undefined;
|
|
71
|
+
loadError = undefined;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Run a wasm call, turning a thrown serde error into a result rather than an
|
|
75
|
+
* exception (trap 3 above). The thrown message is deliberately not passed
|
|
76
|
+
* through verbatim: its positions point into the internal call struct.
|
|
77
|
+
*/
|
|
78
|
+
export function call(what, fn) {
|
|
79
|
+
try {
|
|
80
|
+
return { ok: true, value: fn() };
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
const raw = err instanceof Error ? err.message : String(err);
|
|
84
|
+
// Strip the internal `at line 1 column 42` suffix — it locates a field in
|
|
85
|
+
// the serialized call struct, never anything in the user's policy.
|
|
86
|
+
const message = raw.replace(/\s+at line \d+ column \d+\.?$/, "");
|
|
87
|
+
return { ok: false, message: `${what} could not be run by cedar-wasm: ${message}` };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
/** The first line of a Cedar `DetailedError`, with its `help` when it has one. */
|
|
91
|
+
export function describeError(error) {
|
|
92
|
+
return error.help ? `${error.message} (${error.help})` : error.message;
|
|
93
|
+
}
|
|
94
|
+
// ── Condition bodies ──────────────────────────────────────────────
|
|
95
|
+
/**
|
|
96
|
+
* chant's serializer carries a `when`/`unless` body as Cedar expression
|
|
97
|
+
* *source* under a `__expr` key, because the authored model holds expression
|
|
98
|
+
* strings until schema-driven codegen (#1650) makes them typed trees. `__expr`
|
|
99
|
+
* is not part of Cedar's JSON policy grammar — the deserializer rejects it
|
|
100
|
+
* ("unknown variant `__expr`") — so anything handed to the wasm has to have
|
|
101
|
+
* those bodies parsed into real expression JSON first.
|
|
102
|
+
*
|
|
103
|
+
* There is no expression-level entry point in the package, so the expression
|
|
104
|
+
* is parsed inside a throwaway policy and the resulting condition body lifted
|
|
105
|
+
* back out. A source expression that does not parse is the finding CEDC011
|
|
106
|
+
* reports.
|
|
107
|
+
*/
|
|
108
|
+
export const EXPR_KEY = "__expr";
|
|
109
|
+
function parseExpression(wasm, expression) {
|
|
110
|
+
const answer = call("condition expression", () => wasm.policyToJson(`permit(principal, action, resource) when { ${expression} };`));
|
|
111
|
+
if (!answer.ok)
|
|
112
|
+
return answer;
|
|
113
|
+
if (answer.value.type === "failure") {
|
|
114
|
+
return { ok: false, message: answer.value.errors.map(describeError).join("; ") };
|
|
115
|
+
}
|
|
116
|
+
const conditions = answer.value.json.conditions;
|
|
117
|
+
const body = Array.isArray(conditions) && isRecord(conditions[0]) ? conditions[0].body : undefined;
|
|
118
|
+
if (body === undefined)
|
|
119
|
+
return { ok: false, message: "expression parsed to no condition body" };
|
|
120
|
+
return { ok: true, value: body };
|
|
121
|
+
}
|
|
122
|
+
/** Replace every `__expr` body in a policy set with parsed expression JSON. */
|
|
123
|
+
export function normalizePolicySet(wasm, set) {
|
|
124
|
+
const staticPolicies = {};
|
|
125
|
+
const templates = {};
|
|
126
|
+
const expressionFailures = [];
|
|
127
|
+
for (const entry of set.entries) {
|
|
128
|
+
const target = entry.template ? templates : staticPolicies;
|
|
129
|
+
target[entry.key] = normalizePolicy(wasm, entry.key, entry.policy, expressionFailures);
|
|
130
|
+
}
|
|
131
|
+
const doc = set.doc ?? {};
|
|
132
|
+
const policySet = {
|
|
133
|
+
staticPolicies,
|
|
134
|
+
templates,
|
|
135
|
+
templateLinks: Array.isArray(doc.templateLinks) ? doc.templateLinks : [],
|
|
136
|
+
};
|
|
137
|
+
return { policySet, expressionFailures };
|
|
138
|
+
}
|
|
139
|
+
function normalizePolicy(wasm, key, policy, failures) {
|
|
140
|
+
if (!Array.isArray(policy.conditions))
|
|
141
|
+
return policy;
|
|
142
|
+
const conditions = policy.conditions.map((clause) => {
|
|
143
|
+
if (!isRecord(clause) || !isRecord(clause.body))
|
|
144
|
+
return clause;
|
|
145
|
+
const expression = clause.body[EXPR_KEY];
|
|
146
|
+
if (typeof expression !== "string")
|
|
147
|
+
return clause;
|
|
148
|
+
const parsed = parseExpression(wasm, expression);
|
|
149
|
+
if (!parsed.ok) {
|
|
150
|
+
failures.push({
|
|
151
|
+
key,
|
|
152
|
+
kind: typeof clause.kind === "string" ? clause.kind : "when",
|
|
153
|
+
expression,
|
|
154
|
+
message: parsed.message,
|
|
155
|
+
});
|
|
156
|
+
// Leave a body the deserializer accepts so the rest of the set still
|
|
157
|
+
// validates — the unparseable clause is reported by CEDC011, not by a
|
|
158
|
+
// cascade of confusing structural errors from every other check.
|
|
159
|
+
return { ...clause, body: { Value: true } };
|
|
160
|
+
}
|
|
161
|
+
return { ...clause, body: parsed.value };
|
|
162
|
+
});
|
|
163
|
+
return { ...policy, conditions };
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* The project's Cedar schema, if the build emitted one beside the policies.
|
|
167
|
+
*
|
|
168
|
+
* Two forms, both of which the wasm accepts — but only as the right JS type: a
|
|
169
|
+
* `string` is *always* read as human-readable `.cedarschema`, never as JSON, so
|
|
170
|
+
* a `.cedarschema.json` file has to be `JSON.parse`d into a live object first
|
|
171
|
+
* or the parser reports "unexpected token `{`" against perfectly valid JSON.
|
|
172
|
+
*
|
|
173
|
+
* Reads `ctx.outputs` only. Nothing emits a schema today — schema-driven
|
|
174
|
+
* codegen is #1650 — which is exactly why CEDE010 says so out loud instead of
|
|
175
|
+
* passing silently.
|
|
176
|
+
*/
|
|
177
|
+
export function findSchema(ctx) {
|
|
178
|
+
for (const [, output] of ctx.outputs) {
|
|
179
|
+
if (typeof output === "string")
|
|
180
|
+
continue;
|
|
181
|
+
for (const [filename, content] of Object.entries(output.files ?? {})) {
|
|
182
|
+
if (typeof content !== "string")
|
|
183
|
+
continue;
|
|
184
|
+
if (filename.endsWith(".cedarschema.json")) {
|
|
185
|
+
try {
|
|
186
|
+
const parsed = JSON.parse(content);
|
|
187
|
+
if (isRecord(parsed))
|
|
188
|
+
return { schema: parsed, source: filename };
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
continue; // a schema that is not JSON is not a schema we can use
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
else if (filename.endsWith(".cedarschema")) {
|
|
195
|
+
return { schema: content, source: filename };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return undefined;
|
|
200
|
+
}
|
|
201
|
+
function sortFindings(findings) {
|
|
202
|
+
return [...findings].sort((a, b) => a.policyId.localeCompare(b.policyId) || a.message.localeCompare(b.message));
|
|
203
|
+
}
|
|
204
|
+
function flatten(errors) {
|
|
205
|
+
return errors.map((e) => ({ policyId: e.policyId, message: describeError(e.error) }));
|
|
206
|
+
}
|
|
207
|
+
/** Validate a normalized policy set against a schema. Never throws. */
|
|
208
|
+
export function validatePolicySet(wasm, policySet, schema) {
|
|
209
|
+
const answer = call("validate", () => wasm.validate({ schema, policies: policySet }));
|
|
210
|
+
if (!answer.ok)
|
|
211
|
+
return { errors: [], warnings: [], failure: answer.message };
|
|
212
|
+
if (answer.value.type === "failure") {
|
|
213
|
+
return {
|
|
214
|
+
errors: [],
|
|
215
|
+
warnings: [],
|
|
216
|
+
failure: answer.value.errors.map(describeError).join("; "),
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
// `success` only means validation RAN. The findings are in the arrays.
|
|
220
|
+
return {
|
|
221
|
+
errors: sortFindings(flatten(answer.value.validationErrors)),
|
|
222
|
+
warnings: sortFindings(flatten(answer.value.validationWarnings)),
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
/** Parse-only gate, for when there is no schema to validate against. */
|
|
226
|
+
export function parsePolicySet(wasm, policySet) {
|
|
227
|
+
const answer = call("checkParsePolicySet", () => wasm.checkParsePolicySet(policySet));
|
|
228
|
+
if (!answer.ok)
|
|
229
|
+
return answer;
|
|
230
|
+
if (answer.value.type === "failure") {
|
|
231
|
+
return { ok: true, value: answer.value.errors.map(describeError).sort() };
|
|
232
|
+
}
|
|
233
|
+
return { ok: true, value: [] };
|
|
234
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { cedarPolicyShapeRule } from "./policy-shape.js";
|
|
2
|
+
export { cedarPolicyShapeRule, CEDAR_EFFECTS } from "./policy-shape.js";
|
|
3
|
+
/**
|
|
4
|
+
* All lint rules provided by this lexicon (imported by plugin.ts's lintRules()).
|
|
5
|
+
*
|
|
6
|
+
* Pre-synth rules are wired by hand — unlike post-synth checks, they are not
|
|
7
|
+
* auto-discovered. Add the import above and the entry below.
|
|
8
|
+
*/
|
|
9
|
+
export const rules = [cedarPolicyShapeRule];
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { ts } from "@intentius/chant/lint/typescript";
|
|
2
|
+
/** Module specifiers whose exports construct Cedar declarables. */
|
|
3
|
+
const CEDAR_MODULE = /chant-lexicon-cedar/;
|
|
4
|
+
/** The two effects Cedar has. */
|
|
5
|
+
export const CEDAR_EFFECTS = ["permit", "forbid"];
|
|
6
|
+
function importedCedarBindings(sourceFile) {
|
|
7
|
+
const names = new Set();
|
|
8
|
+
for (const statement of sourceFile.statements) {
|
|
9
|
+
if (!ts.isImportDeclaration(statement))
|
|
10
|
+
continue;
|
|
11
|
+
if (!ts.isStringLiteral(statement.moduleSpecifier))
|
|
12
|
+
continue;
|
|
13
|
+
if (!CEDAR_MODULE.test(statement.moduleSpecifier.text))
|
|
14
|
+
continue;
|
|
15
|
+
const bindings = statement.importClause?.namedBindings;
|
|
16
|
+
if (bindings && ts.isNamedImports(bindings)) {
|
|
17
|
+
for (const element of bindings.elements)
|
|
18
|
+
names.add(element.name.text);
|
|
19
|
+
}
|
|
20
|
+
if (statement.importClause?.name)
|
|
21
|
+
names.add(statement.importClause.name.text);
|
|
22
|
+
}
|
|
23
|
+
return names;
|
|
24
|
+
}
|
|
25
|
+
/** The callee identifier of a `new X(…)` / `X(…)` expression, if it has one. */
|
|
26
|
+
function calleeName(node) {
|
|
27
|
+
return ts.isIdentifier(node.expression) ? node.expression.text : undefined;
|
|
28
|
+
}
|
|
29
|
+
/** The last object-literal argument — where chant resources carry their props. */
|
|
30
|
+
function propsLiteral(node) {
|
|
31
|
+
const args = [...(node.arguments ?? [])];
|
|
32
|
+
for (let i = args.length - 1; i >= 0; i--) {
|
|
33
|
+
const arg = args[i];
|
|
34
|
+
if (ts.isObjectLiteralExpression(arg))
|
|
35
|
+
return arg;
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
function property(obj, key) {
|
|
40
|
+
for (const prop of obj.properties) {
|
|
41
|
+
if (!ts.isPropertyAssignment(prop))
|
|
42
|
+
continue;
|
|
43
|
+
if (!ts.isIdentifier(prop.name) && !ts.isStringLiteral(prop.name))
|
|
44
|
+
continue;
|
|
45
|
+
if (prop.name.text === key)
|
|
46
|
+
return prop;
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
function lineCol(sourceFile, node) {
|
|
51
|
+
const { line, character } = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
52
|
+
return { line: line + 1, column: character + 1 };
|
|
53
|
+
}
|
|
54
|
+
export const cedarPolicyShapeRule = {
|
|
55
|
+
id: "CEDC001",
|
|
56
|
+
severity: "error",
|
|
57
|
+
category: "correctness",
|
|
58
|
+
description: "A Cedar policy declaration must use effect permit or forbid, and must not carry an empty when/unless clause",
|
|
59
|
+
check(context) {
|
|
60
|
+
const { sourceFile } = context;
|
|
61
|
+
const diagnostics = [];
|
|
62
|
+
const cedarBindings = importedCedarBindings(sourceFile);
|
|
63
|
+
if (cedarBindings.size === 0)
|
|
64
|
+
return diagnostics;
|
|
65
|
+
const report = (node, message) => {
|
|
66
|
+
const { line, column } = lineCol(sourceFile, node);
|
|
67
|
+
diagnostics.push({
|
|
68
|
+
file: sourceFile.fileName,
|
|
69
|
+
line,
|
|
70
|
+
column,
|
|
71
|
+
ruleId: "CEDC001",
|
|
72
|
+
severity: "error",
|
|
73
|
+
message,
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
function inspect(node) {
|
|
77
|
+
const callee = calleeName(node);
|
|
78
|
+
if (!callee || !cedarBindings.has(callee))
|
|
79
|
+
return;
|
|
80
|
+
const props = propsLiteral(node);
|
|
81
|
+
if (!props)
|
|
82
|
+
return;
|
|
83
|
+
const effect = property(props, "effect");
|
|
84
|
+
if (effect && ts.isStringLiteral(effect.initializer)) {
|
|
85
|
+
const value = effect.initializer.text;
|
|
86
|
+
if (!CEDAR_EFFECTS.includes(value)) {
|
|
87
|
+
report(effect, `Cedar policy effect "${value}" is not a Cedar effect — the language has only permit and forbid, and anything that is not "forbid" is emitted as a permit. Write effect: "forbid" if that is what was meant.`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
for (const clause of ["when", "unless"]) {
|
|
91
|
+
const guard = property(props, clause);
|
|
92
|
+
if (!guard)
|
|
93
|
+
continue;
|
|
94
|
+
const entries = ts.isArrayLiteralExpression(guard.initializer)
|
|
95
|
+
? guard.initializer.elements
|
|
96
|
+
: [guard.initializer];
|
|
97
|
+
for (const entry of entries) {
|
|
98
|
+
if (!ts.isStringLiteral(entry) && !ts.isNoSubstitutionTemplateLiteral(entry))
|
|
99
|
+
continue;
|
|
100
|
+
if (entry.text.trim() !== "")
|
|
101
|
+
continue;
|
|
102
|
+
report(entry, `Cedar policy has an empty ${clause} clause. An empty guard serializes to ${clause} { }, which is not a Cedar expression and makes the whole policy set unparseable — write the condition, or drop the entry.`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function visit(node) {
|
|
107
|
+
if (ts.isCallExpression(node) || ts.isNewExpression(node))
|
|
108
|
+
inspect(node);
|
|
109
|
+
ts.forEachChild(node, visit);
|
|
110
|
+
}
|
|
111
|
+
visit(sourceFile);
|
|
112
|
+
return diagnostics;
|
|
113
|
+
},
|
|
114
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { lexiconCompletions } from "@intentius/chant/lsp/lexicon-providers";
|
|
2
|
+
import { cedarIndex, cedarRegistry } from "./registry.js";
|
|
3
|
+
/**
|
|
4
|
+
* Completions over the schema-derived registry.
|
|
5
|
+
*
|
|
6
|
+
* Two shapes come out of core's generic provider: class names after `new `,
|
|
7
|
+
* and property names inside a constructor's object literal. Both are useful
|
|
8
|
+
* here — `new Policy({` completes `effect`/`principal`/`action`/…, and
|
|
9
|
+
* `new Doc` completes `Document`.
|
|
10
|
+
*
|
|
11
|
+
* The third shape is Cedar's own: a scope names an *action constant*, not a
|
|
12
|
+
* class, and those are plain `const` exports rather than constructors, so no
|
|
13
|
+
* `new ` ever precedes them. {@link actionCompletions} covers that position.
|
|
14
|
+
*/
|
|
15
|
+
export function completions(ctx) {
|
|
16
|
+
const generic = lexiconCompletions(ctx, cedarIndex(), "Cedar declaration");
|
|
17
|
+
if (generic.length > 0)
|
|
18
|
+
return generic;
|
|
19
|
+
return actionCompletions(ctx);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Action constants in a scope position.
|
|
23
|
+
*
|
|
24
|
+
* `action: { eq: <cursor> }` and `action: { in: [<cursor>` are where an action
|
|
25
|
+
* is named, and the generated constants (`ReadAction`, `WriteAction`, …) are
|
|
26
|
+
* the only correct values. Matching on the surrounding text rather than on a
|
|
27
|
+
* parsed AST keeps this cheap and is what the other lexicons' providers do.
|
|
28
|
+
*/
|
|
29
|
+
export function actionCompletions(ctx) {
|
|
30
|
+
const linePrefix = ctx.linePrefix ?? "";
|
|
31
|
+
if (!/\baction\s*:\s*\{\s*(eq|in)\s*:\s*\[?\s*[A-Za-z]*$/.test(linePrefix))
|
|
32
|
+
return [];
|
|
33
|
+
const prefix = (ctx.wordAtCursor ?? "").toLowerCase();
|
|
34
|
+
const registry = cedarRegistry();
|
|
35
|
+
return Object.entries(registry)
|
|
36
|
+
.filter(([, entry]) => entry.kind === "resource" && entry.resourceType.includes("::Action::"))
|
|
37
|
+
.filter(([className]) => !prefix || className.toLowerCase().startsWith(prefix))
|
|
38
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
39
|
+
.map(([className, entry]) => ({
|
|
40
|
+
label: className,
|
|
41
|
+
insertText: className,
|
|
42
|
+
kind: "resource",
|
|
43
|
+
detail: entry.resourceType,
|
|
44
|
+
documentation: [
|
|
45
|
+
`Cedar action: \`${entry.resourceType}\``,
|
|
46
|
+
entry.principalTypes?.length ? `Principals: ${entry.principalTypes.join(", ")}` : "",
|
|
47
|
+
entry.resourceTypes?.length ? `Resources: ${entry.resourceTypes.join(", ")}` : "",
|
|
48
|
+
]
|
|
49
|
+
.filter(Boolean)
|
|
50
|
+
.join("\n"),
|
|
51
|
+
}));
|
|
52
|
+
}
|