@intentius/chant-lexicon-cedar 0.124.30 → 0.124.32
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,59 @@
|
|
|
1
|
+
import { CEDAR_LEXICON, policySets } from "./cedar-helpers.js";
|
|
2
|
+
import { loadWasm, normalizePolicySet, parsePolicySet, wasmLoadError } from "./wasm-helpers.js";
|
|
3
|
+
export const cedc010 = {
|
|
4
|
+
id: "CEDC010",
|
|
5
|
+
description: "The emitted Cedar policy set must parse as Cedar (checkParsePolicySet)",
|
|
6
|
+
check(ctx) {
|
|
7
|
+
const diagnostics = [];
|
|
8
|
+
const sets = policySets(ctx);
|
|
9
|
+
if (sets.length === 0)
|
|
10
|
+
return diagnostics;
|
|
11
|
+
const wasm = loadWasm();
|
|
12
|
+
if (!wasm) {
|
|
13
|
+
// The only place the missing-validator condition is reported. The other
|
|
14
|
+
// wasm-backed checks stay quiet rather than repeating it four times.
|
|
15
|
+
return [
|
|
16
|
+
{
|
|
17
|
+
checkId: "CEDC010",
|
|
18
|
+
severity: "warning",
|
|
19
|
+
message: `Cedar policies were emitted but @cedar-policy/cedar-wasm could not be loaded, so none of them were parsed or validated: ${wasmLoadError()}`,
|
|
20
|
+
lexicon: CEDAR_LEXICON,
|
|
21
|
+
},
|
|
22
|
+
];
|
|
23
|
+
}
|
|
24
|
+
for (const set of sets) {
|
|
25
|
+
if (set.doc === undefined) {
|
|
26
|
+
diagnostics.push({
|
|
27
|
+
checkId: "CEDC010",
|
|
28
|
+
severity: "error",
|
|
29
|
+
message: `Cedar policy set "${set.source}" is not readable as JSON: ${set.parseError}`,
|
|
30
|
+
entity: set.source,
|
|
31
|
+
lexicon: set.lexicon,
|
|
32
|
+
});
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
const { policySet } = normalizePolicySet(wasm, set);
|
|
36
|
+
const parsed = parsePolicySet(wasm, policySet);
|
|
37
|
+
if (!parsed.ok) {
|
|
38
|
+
diagnostics.push({
|
|
39
|
+
checkId: "CEDC010",
|
|
40
|
+
severity: "error",
|
|
41
|
+
message: `Cedar policy set "${set.source}" could not be parsed: ${parsed.message}`,
|
|
42
|
+
entity: set.source,
|
|
43
|
+
lexicon: set.lexicon,
|
|
44
|
+
});
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
for (const message of parsed.value) {
|
|
48
|
+
diagnostics.push({
|
|
49
|
+
checkId: "CEDC010",
|
|
50
|
+
severity: "error",
|
|
51
|
+
message: `Cedar policy set "${set.source}" does not parse as Cedar: ${message}`,
|
|
52
|
+
entity: set.source,
|
|
53
|
+
lexicon: set.lexicon,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return diagnostics;
|
|
58
|
+
},
|
|
59
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { CEDAR_LEXICON, parsedPolicySets } from "./cedar-helpers.js";
|
|
2
|
+
import { loadWasm, normalizePolicySet } from "./wasm-helpers.js";
|
|
3
|
+
export const cedc011 = {
|
|
4
|
+
id: "CEDC011",
|
|
5
|
+
description: "Every when/unless condition expression in an emitted policy must parse as a Cedar expression",
|
|
6
|
+
check(ctx) {
|
|
7
|
+
const diagnostics = [];
|
|
8
|
+
const sets = parsedPolicySets(ctx);
|
|
9
|
+
if (sets.length === 0)
|
|
10
|
+
return diagnostics;
|
|
11
|
+
// A missing validator is CEDC010's finding to report, not this one's.
|
|
12
|
+
const wasm = loadWasm();
|
|
13
|
+
if (!wasm)
|
|
14
|
+
return diagnostics;
|
|
15
|
+
for (const set of sets) {
|
|
16
|
+
const { expressionFailures } = normalizePolicySet(wasm, set);
|
|
17
|
+
for (const failure of expressionFailures) {
|
|
18
|
+
diagnostics.push({
|
|
19
|
+
checkId: "CEDC011",
|
|
20
|
+
severity: "error",
|
|
21
|
+
message: `Policy "${failure.key}" has a ${failure.kind} clause whose expression does not parse: ${failure.expression} — ${failure.message}`,
|
|
22
|
+
entity: failure.key,
|
|
23
|
+
lexicon: set.lexicon || CEDAR_LEXICON,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return diagnostics;
|
|
28
|
+
},
|
|
29
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { annotatedId, parsedPolicySets } from "./cedar-helpers.js";
|
|
2
|
+
export const cedc012 = {
|
|
3
|
+
id: "CEDC012",
|
|
4
|
+
description: "No two emitted Cedar policies may claim the same policy id",
|
|
5
|
+
check(ctx) {
|
|
6
|
+
const claims = new Map();
|
|
7
|
+
for (const set of parsedPolicySets(ctx)) {
|
|
8
|
+
for (const entry of set.entries) {
|
|
9
|
+
// The key and the `@id` annotation are two ways of claiming the same
|
|
10
|
+
// id; a policy claiming both only counts once.
|
|
11
|
+
const ids = new Set([entry.key, annotatedId(entry.policy)].filter((id) => !!id));
|
|
12
|
+
for (const id of ids) {
|
|
13
|
+
const existing = claims.get(id) ?? [];
|
|
14
|
+
existing.push({ lexicon: set.lexicon, source: set.source, key: entry.key });
|
|
15
|
+
claims.set(id, existing);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const diagnostics = [];
|
|
20
|
+
for (const [id, holders] of [...claims].sort(([a], [b]) => a.localeCompare(b))) {
|
|
21
|
+
if (holders.length < 2)
|
|
22
|
+
continue;
|
|
23
|
+
const where = holders.map((h) => `${h.source}:${h.key}`).join(", ");
|
|
24
|
+
diagnostics.push({
|
|
25
|
+
checkId: "CEDC012",
|
|
26
|
+
severity: "error",
|
|
27
|
+
message: `Cedar policy id "${id}" is claimed by ${holders.length} policies (${where}). Ids must be unique across the policy set — rename one of the declarations or give it an explicit, distinct @id annotation.`,
|
|
28
|
+
entity: id,
|
|
29
|
+
lexicon: holders[0].lexicon,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
return diagnostics;
|
|
33
|
+
},
|
|
34
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { annotationsOf, parsedPolicySets } from "./cedar-helpers.js";
|
|
2
|
+
export const cedc013 = {
|
|
3
|
+
id: "CEDC013",
|
|
4
|
+
description: "An emitted policy's @id annotation must match its policy id, and annotation values must be non-empty strings",
|
|
5
|
+
check(ctx) {
|
|
6
|
+
const diagnostics = [];
|
|
7
|
+
for (const set of parsedPolicySets(ctx)) {
|
|
8
|
+
for (const entry of set.entries) {
|
|
9
|
+
const annotations = annotationsOf(entry.policy);
|
|
10
|
+
for (const [key, value] of Object.entries(annotations)) {
|
|
11
|
+
if (typeof value !== "string") {
|
|
12
|
+
diagnostics.push({
|
|
13
|
+
checkId: "CEDC013",
|
|
14
|
+
severity: "warning",
|
|
15
|
+
message: `Policy "${entry.key}" has a non-string @${key} annotation (${JSON.stringify(value)}). Cedar annotations are strings.`,
|
|
16
|
+
entity: entry.key,
|
|
17
|
+
lexicon: set.lexicon,
|
|
18
|
+
});
|
|
19
|
+
continue;
|
|
20
|
+
}
|
|
21
|
+
if (value.trim() === "") {
|
|
22
|
+
diagnostics.push({
|
|
23
|
+
checkId: "CEDC013",
|
|
24
|
+
severity: "warning",
|
|
25
|
+
message: `Policy "${entry.key}" has an empty @${key} annotation. Give it a value or drop it — an empty annotation reads as documentation and carries none.`,
|
|
26
|
+
entity: entry.key,
|
|
27
|
+
lexicon: set.lexicon,
|
|
28
|
+
});
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (key === "id" && value !== entry.key) {
|
|
32
|
+
diagnostics.push({
|
|
33
|
+
checkId: "CEDC013",
|
|
34
|
+
severity: "warning",
|
|
35
|
+
message: `Policy filed under id "${entry.key}" annotates @id("${value}"). The .cedar text and the JSON policy set would name this policy differently — make the annotation match, or remove it and let chant derive the id.`,
|
|
36
|
+
entity: entry.key,
|
|
37
|
+
lexicon: set.lexicon,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return diagnostics;
|
|
44
|
+
},
|
|
45
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { isRecord, parsedPolicySets, scopesOf } from "./cedar-helpers.js";
|
|
2
|
+
/** Every entity reference a scope position carries, in document order. */
|
|
3
|
+
function scopeEntities(scope) {
|
|
4
|
+
const found = [];
|
|
5
|
+
const collect = (holder) => {
|
|
6
|
+
if (!isRecord(holder))
|
|
7
|
+
return;
|
|
8
|
+
if (holder.entity !== undefined)
|
|
9
|
+
found.push(holder.entity);
|
|
10
|
+
if (Array.isArray(holder.entities))
|
|
11
|
+
found.push(...holder.entities);
|
|
12
|
+
};
|
|
13
|
+
collect(scope);
|
|
14
|
+
// `is T in E` nests the `in` constraint under the `is`.
|
|
15
|
+
collect(scope.in);
|
|
16
|
+
return found;
|
|
17
|
+
}
|
|
18
|
+
export const cedc014 = {
|
|
19
|
+
id: "CEDC014",
|
|
20
|
+
description: "An entity reference in a policy scope must be a well-formed Namespace::Type::\"id\" UID",
|
|
21
|
+
check(ctx) {
|
|
22
|
+
const diagnostics = [];
|
|
23
|
+
for (const set of parsedPolicySets(ctx)) {
|
|
24
|
+
for (const entry of set.entries) {
|
|
25
|
+
for (const { variable, scope } of scopesOf(entry.policy)) {
|
|
26
|
+
for (const ref of scopeEntities(scope)) {
|
|
27
|
+
if (!isRecord(ref)) {
|
|
28
|
+
diagnostics.push({
|
|
29
|
+
checkId: "CEDC014",
|
|
30
|
+
severity: "error",
|
|
31
|
+
message: `Policy "${entry.key}" has a ${variable} constraint whose entity reference is not an entity UID (${JSON.stringify(ref)}).`,
|
|
32
|
+
entity: entry.key,
|
|
33
|
+
lexicon: set.lexicon,
|
|
34
|
+
});
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const type = typeof ref.type === "string" ? ref.type : "";
|
|
38
|
+
const id = typeof ref.id === "string" ? ref.id : undefined;
|
|
39
|
+
if (type === "" || id === undefined || id === "") {
|
|
40
|
+
diagnostics.push({
|
|
41
|
+
checkId: "CEDC014",
|
|
42
|
+
severity: "error",
|
|
43
|
+
message: `Policy "${entry.key}" constrains ${variable} against "${type}" with no entity id. Write the reference as a full UID — Namespace::Type::"id" — so it resolves to an entity rather than to a bare type name.`,
|
|
44
|
+
entity: entry.key,
|
|
45
|
+
lexicon: set.lexicon,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return diagnostics;
|
|
53
|
+
},
|
|
54
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { parsedPolicySets } from "./cedar-helpers.js";
|
|
2
|
+
import { findSchema, loadWasm, normalizePolicySet, validatePolicySet } from "./wasm-helpers.js";
|
|
3
|
+
export const cede010 = {
|
|
4
|
+
id: "CEDE010",
|
|
5
|
+
description: "Every emitted Cedar policy set validates clean against the project schema (cedar-wasm validate)",
|
|
6
|
+
check(ctx) {
|
|
7
|
+
const diagnostics = [];
|
|
8
|
+
const sets = parsedPolicySets(ctx);
|
|
9
|
+
if (sets.length === 0)
|
|
10
|
+
return diagnostics;
|
|
11
|
+
// A missing validator is CEDC010's finding to report, not this one's.
|
|
12
|
+
const wasm = loadWasm();
|
|
13
|
+
if (!wasm)
|
|
14
|
+
return diagnostics;
|
|
15
|
+
const schema = findSchema(ctx);
|
|
16
|
+
if (!schema) {
|
|
17
|
+
return [
|
|
18
|
+
{
|
|
19
|
+
checkId: "CEDE010",
|
|
20
|
+
severity: "info",
|
|
21
|
+
message: `No Cedar schema was emitted with the ${sets.length} policy set(s) in this build, so they were checked for parseability only — nothing verified that the entity types, actions, and attributes they name exist. Emit a .cedarschema (or .cedarschema.json) alongside the policies to turn this into real validation.`,
|
|
22
|
+
lexicon: sets[0].lexicon,
|
|
23
|
+
},
|
|
24
|
+
];
|
|
25
|
+
}
|
|
26
|
+
for (const set of sets) {
|
|
27
|
+
const { policySet } = normalizePolicySet(wasm, set);
|
|
28
|
+
const outcome = validatePolicySet(wasm, policySet, schema.schema);
|
|
29
|
+
if (outcome.failure) {
|
|
30
|
+
diagnostics.push({
|
|
31
|
+
checkId: "CEDE010",
|
|
32
|
+
severity: "error",
|
|
33
|
+
message: `Cedar policy set "${set.source}" could not be validated against "${schema.source}": ${outcome.failure}`,
|
|
34
|
+
entity: set.source,
|
|
35
|
+
lexicon: set.lexicon,
|
|
36
|
+
});
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
for (const finding of outcome.errors) {
|
|
40
|
+
diagnostics.push({
|
|
41
|
+
checkId: "CEDE010",
|
|
42
|
+
severity: "error",
|
|
43
|
+
message: `Policy "${finding.policyId}" fails Cedar validation against "${schema.source}": ${finding.message}`,
|
|
44
|
+
entity: finding.policyId,
|
|
45
|
+
lexicon: set.lexicon,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return diagnostics;
|
|
50
|
+
},
|
|
51
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { parsedPolicySets } from "./cedar-helpers.js";
|
|
2
|
+
import { findSchema, loadWasm, normalizePolicySet, validatePolicySet } from "./wasm-helpers.js";
|
|
3
|
+
export const cede011 = {
|
|
4
|
+
id: "CEDE011",
|
|
5
|
+
description: "Cedar validation warnings (impossible policies, shadowed conditions) on an emitted policy set",
|
|
6
|
+
check(ctx) {
|
|
7
|
+
const diagnostics = [];
|
|
8
|
+
const sets = parsedPolicySets(ctx);
|
|
9
|
+
if (sets.length === 0)
|
|
10
|
+
return diagnostics;
|
|
11
|
+
// Both "no validator" and "no schema" are already reported once by
|
|
12
|
+
// CEDC010/CEDE010; repeating them here would triple the same line.
|
|
13
|
+
const wasm = loadWasm();
|
|
14
|
+
if (!wasm)
|
|
15
|
+
return diagnostics;
|
|
16
|
+
const schema = findSchema(ctx);
|
|
17
|
+
if (!schema)
|
|
18
|
+
return diagnostics;
|
|
19
|
+
for (const set of sets) {
|
|
20
|
+
const { policySet } = normalizePolicySet(wasm, set);
|
|
21
|
+
const outcome = validatePolicySet(wasm, policySet, schema.schema);
|
|
22
|
+
if (outcome.failure)
|
|
23
|
+
continue; // CEDE010 reports the failed call
|
|
24
|
+
for (const finding of outcome.warnings) {
|
|
25
|
+
diagnostics.push({
|
|
26
|
+
checkId: "CEDE011",
|
|
27
|
+
severity: "warning",
|
|
28
|
+
message: `Policy "${finding.policyId}" validates but Cedar warns about it: ${finding.message}`,
|
|
29
|
+
entity: finding.policyId,
|
|
30
|
+
lexicon: set.lexicon,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return diagnostics;
|
|
35
|
+
},
|
|
36
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { conditionsOf, effectOf, isProdLikeEnv, parsedPolicySets, scopeIsAll } from "./cedar-helpers.js";
|
|
2
|
+
export const ceds010 = {
|
|
3
|
+
id: "CEDS010",
|
|
4
|
+
description: "A bare permit(principal, action, resource) with no scope constraints and no conditions grants everything",
|
|
5
|
+
check(ctx) {
|
|
6
|
+
const diagnostics = [];
|
|
7
|
+
const prod = isProdLikeEnv(ctx.env);
|
|
8
|
+
const severity = prod ? "error" : "warning";
|
|
9
|
+
for (const set of parsedPolicySets(ctx)) {
|
|
10
|
+
for (const entry of set.entries) {
|
|
11
|
+
if (effectOf(entry.policy) !== "permit")
|
|
12
|
+
continue;
|
|
13
|
+
const policy = entry.policy;
|
|
14
|
+
const bare = scopeIsAll(policy.principal) &&
|
|
15
|
+
scopeIsAll(policy.action) &&
|
|
16
|
+
scopeIsAll(policy.resource) &&
|
|
17
|
+
conditionsOf(policy).length === 0;
|
|
18
|
+
if (!bare)
|
|
19
|
+
continue;
|
|
20
|
+
diagnostics.push({
|
|
21
|
+
checkId: "CEDS010",
|
|
22
|
+
severity,
|
|
23
|
+
message: `Policy "${entry.key}" is a bare permit(principal, action, resource) — every principal may take every action on every resource.${prod ? "" : " This is an error in a production build."} Constrain a scope or add a when/unless guard.`,
|
|
24
|
+
entity: entry.key,
|
|
25
|
+
lexicon: set.lexicon,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return diagnostics;
|
|
30
|
+
},
|
|
31
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { effectOf, parsedPolicySets } from "./cedar-helpers.js";
|
|
2
|
+
export const ceds011 = {
|
|
3
|
+
id: "CEDS011",
|
|
4
|
+
description: "A policy set built entirely from permits carries no forbid to bound them",
|
|
5
|
+
check(ctx) {
|
|
6
|
+
const diagnostics = [];
|
|
7
|
+
for (const set of parsedPolicySets(ctx)) {
|
|
8
|
+
if (set.entries.length === 0)
|
|
9
|
+
continue;
|
|
10
|
+
if (set.entries.some((entry) => effectOf(entry.policy) === "forbid"))
|
|
11
|
+
continue;
|
|
12
|
+
diagnostics.push({
|
|
13
|
+
checkId: "CEDS011",
|
|
14
|
+
severity: "warning",
|
|
15
|
+
message: `Cedar policy set "${set.source}" has ${set.entries.length} policy/policies and no forbid. A forbid beats every permit regardless of order, so it is the only way to state an invariant a later grant cannot widen — add one for whatever this set must never allow.`,
|
|
16
|
+
entity: set.source,
|
|
17
|
+
lexicon: set.lexicon,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
return diagnostics;
|
|
21
|
+
},
|
|
22
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { conditionsOf, effectOf, parsedPolicySets, scopeIsAll } from "./cedar-helpers.js";
|
|
2
|
+
export const ceds012 = {
|
|
3
|
+
id: "CEDS012",
|
|
4
|
+
description: "A permit that leaves the action scope unconstrained grants every action, including future ones",
|
|
5
|
+
check(ctx) {
|
|
6
|
+
const diagnostics = [];
|
|
7
|
+
for (const set of parsedPolicySets(ctx)) {
|
|
8
|
+
for (const entry of set.entries) {
|
|
9
|
+
const policy = entry.policy;
|
|
10
|
+
if (effectOf(policy) !== "permit")
|
|
11
|
+
continue;
|
|
12
|
+
if (!scopeIsAll(policy.action))
|
|
13
|
+
continue;
|
|
14
|
+
// The everything-permit belongs to CEDS010.
|
|
15
|
+
const narrowedElsewhere = !scopeIsAll(policy.principal) ||
|
|
16
|
+
!scopeIsAll(policy.resource) ||
|
|
17
|
+
conditionsOf(policy).length > 0;
|
|
18
|
+
if (!narrowedElsewhere)
|
|
19
|
+
continue;
|
|
20
|
+
diagnostics.push({
|
|
21
|
+
checkId: "CEDS012",
|
|
22
|
+
severity: "warning",
|
|
23
|
+
message: `Policy "${entry.key}" permits every action — the action scope is unconstrained, so the grant widens on its own as the schema gains actions. Name the actions it needs (action == Action::"read", or action in [ … ]).`,
|
|
24
|
+
entity: entry.key,
|
|
25
|
+
lexicon: set.lexicon,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return diagnostics;
|
|
30
|
+
},
|
|
31
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { blankComments, dogwoodPolicyFiles, dogwoodSchemaFiles } from "../../dogwood/scan.js";
|
|
2
|
+
/**
|
|
3
|
+
* A `.dw` file holding only `def` declarations is a macro library, not a
|
|
4
|
+
* policy set.
|
|
5
|
+
*
|
|
6
|
+
* By structure rather than by filename: the serializer's default is
|
|
7
|
+
* `macros.dw`, but `MacroLibraryProps.filename` overrides it, and `chant
|
|
8
|
+
* audit` runs over trees chant never wrote. A policy set is a file with a
|
|
9
|
+
* `permit` or `forbid` head in it — upstream's policy grammar has no third
|
|
10
|
+
* effect — and a set with inlined macros still has one.
|
|
11
|
+
*/
|
|
12
|
+
export function looksLikeMacroLibrary(text) {
|
|
13
|
+
return !/\b(permit|forbid)\s*\(/.test(blankComments(text));
|
|
14
|
+
}
|
|
15
|
+
function byName(a, b) {
|
|
16
|
+
return a.source.localeCompare(b.source);
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The Cedar action schema the CLI needs, as text.
|
|
20
|
+
*
|
|
21
|
+
* `--policy-schema` reads a human-readable `.cedarschema`; a `.cedarschema.json`
|
|
22
|
+
* is not a substitute, so it is not offered as one. Sorted by filename so a
|
|
23
|
+
* build emitting several picks the same one on every run.
|
|
24
|
+
*/
|
|
25
|
+
function findPolicySchema(ctx) {
|
|
26
|
+
const found = [];
|
|
27
|
+
for (const [lexicon, output] of ctx.outputs) {
|
|
28
|
+
if (typeof output === "string")
|
|
29
|
+
continue;
|
|
30
|
+
for (const [source, text] of Object.entries(output.files ?? {})) {
|
|
31
|
+
if (typeof text !== "string")
|
|
32
|
+
continue;
|
|
33
|
+
if (!source.endsWith(".cedarschema"))
|
|
34
|
+
continue;
|
|
35
|
+
found.push({ lexicon, source, text });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return found.sort(byName)[0];
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Plan every `dogwood` invocation this build calls for.
|
|
42
|
+
*
|
|
43
|
+
* One bundle per policy set per emitted event schema. A build with several
|
|
44
|
+
* `.dwschema` files means several services, and `--event-schema` takes exactly
|
|
45
|
+
* one file, so the set is run against each and every finding names the schema
|
|
46
|
+
* it was found under — the alternative is picking one silently and reporting
|
|
47
|
+
* against a schema the reader did not choose. Macro libraries do combine: a
|
|
48
|
+
* library is a sequence of `def`s, so several concatenate into the one file
|
|
49
|
+
* `--macros` accepts.
|
|
50
|
+
*/
|
|
51
|
+
export function planDogwoodRuns(ctx) {
|
|
52
|
+
const files = dogwoodPolicyFiles(ctx).sort(byName);
|
|
53
|
+
const policySets = files.filter((f) => !looksLikeMacroLibrary(f.text));
|
|
54
|
+
if (policySets.length === 0)
|
|
55
|
+
return { hasPolicies: false, bundles: [] };
|
|
56
|
+
const lexicon = policySets[0].lexicon;
|
|
57
|
+
const libraries = files.filter((f) => looksLikeMacroLibrary(f.text));
|
|
58
|
+
const macros = libraries.length > 0 ? libraries.map((f) => f.text).join("\n") : undefined;
|
|
59
|
+
const policySchema = findPolicySchema(ctx);
|
|
60
|
+
if (!policySchema) {
|
|
61
|
+
return {
|
|
62
|
+
hasPolicies: true,
|
|
63
|
+
bundles: [],
|
|
64
|
+
lexicon,
|
|
65
|
+
blocked: "no Cedar action schema (.cedarschema) was emitted beside them, and `dogwood validate` requires one via --policy-schema",
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const eventSchemas = dogwoodSchemaFiles(ctx).sort(byName);
|
|
69
|
+
const bundles = [];
|
|
70
|
+
for (const set of policySets) {
|
|
71
|
+
for (const events of eventSchemas.length > 0 ? eventSchemas : [undefined]) {
|
|
72
|
+
bundles.push({
|
|
73
|
+
lexicon: set.lexicon,
|
|
74
|
+
source: set.source,
|
|
75
|
+
policySchemaSource: policySchema.source,
|
|
76
|
+
...(events ? { eventSchemaSource: events.source } : {}),
|
|
77
|
+
bundle: {
|
|
78
|
+
policies: set.text,
|
|
79
|
+
policySchema: policySchema.text,
|
|
80
|
+
...(events ? { eventSchema: events.text } : {}),
|
|
81
|
+
...(macros !== undefined ? { macros } : {}),
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return { hasPolicies: true, bundles, lexicon };
|
|
87
|
+
}
|
|
88
|
+
/** How a finding names the inputs a run was made with. */
|
|
89
|
+
export function describeBundle(prepared) {
|
|
90
|
+
const parts = [`"${prepared.source}"`, `against "${prepared.policySchemaSource}"`];
|
|
91
|
+
if (prepared.eventSchemaSource)
|
|
92
|
+
parts.push(`with "${prepared.eventSchemaSource}"`);
|
|
93
|
+
return parts.join(" ");
|
|
94
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { dogwoodPolicyFiles, dogwoodSchemaFiles, readEventSchema, scanPredicates } from "../../dogwood/scan.js";
|
|
2
|
+
export const dwdc010 = {
|
|
3
|
+
id: "DWDC010",
|
|
4
|
+
description: "A dogwood temporal predicate names an event kind the emitted .dwschema declares",
|
|
5
|
+
check(ctx) {
|
|
6
|
+
const schemas = dogwoodSchemaFiles(ctx);
|
|
7
|
+
if (schemas.length === 0)
|
|
8
|
+
return [];
|
|
9
|
+
const declared = new Set();
|
|
10
|
+
for (const schema of schemas) {
|
|
11
|
+
for (const kind of readEventSchema(schema.text).kinds)
|
|
12
|
+
declared.add(kind);
|
|
13
|
+
}
|
|
14
|
+
if (declared.size === 0)
|
|
15
|
+
return [];
|
|
16
|
+
const known = [...declared].sort().join(", ");
|
|
17
|
+
const diagnostics = [];
|
|
18
|
+
for (const policies of dogwoodPolicyFiles(ctx)) {
|
|
19
|
+
const seen = new Set();
|
|
20
|
+
for (const ref of scanPredicates(policies.text)) {
|
|
21
|
+
if (declared.has(ref.kind))
|
|
22
|
+
continue;
|
|
23
|
+
const key = `${ref.action}::${ref.kind}`;
|
|
24
|
+
if (seen.has(key))
|
|
25
|
+
continue;
|
|
26
|
+
seen.add(key);
|
|
27
|
+
diagnostics.push({
|
|
28
|
+
checkId: "DWDC010",
|
|
29
|
+
severity: "error",
|
|
30
|
+
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}).`,
|
|
31
|
+
entity: policies.source,
|
|
32
|
+
lexicon: policies.lexicon,
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return diagnostics;
|
|
37
|
+
},
|
|
38
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { dogwoodPolicyFiles, dogwoodSchemaFiles, effectiveMaxWindowSeconds, readEventSchema, scanWindows, } from "../../dogwood/scan.js";
|
|
2
|
+
export const dwdc011 = {
|
|
3
|
+
id: "DWDC011",
|
|
4
|
+
description: "A dogwood temporal window stays inside the event schema's max_window cap",
|
|
5
|
+
check(ctx) {
|
|
6
|
+
const policyFiles = dogwoodPolicyFiles(ctx);
|
|
7
|
+
if (policyFiles.length === 0)
|
|
8
|
+
return [];
|
|
9
|
+
const schemas = dogwoodSchemaFiles(ctx).map((s) => readEventSchema(s.text));
|
|
10
|
+
const cap = effectiveMaxWindowSeconds(schemas);
|
|
11
|
+
const source = schemas.length === 0
|
|
12
|
+
? "upstream's default cap (no event schema was emitted)"
|
|
13
|
+
: `the emitted event schema's max_window`;
|
|
14
|
+
const diagnostics = [];
|
|
15
|
+
for (const policies of policyFiles) {
|
|
16
|
+
const reported = new Set();
|
|
17
|
+
for (const window of scanWindows(policies.text)) {
|
|
18
|
+
if (window.seconds <= cap.seconds)
|
|
19
|
+
continue;
|
|
20
|
+
if (reported.has(window.text))
|
|
21
|
+
continue;
|
|
22
|
+
reported.add(window.text);
|
|
23
|
+
diagnostics.push({
|
|
24
|
+
checkId: "DWDC011",
|
|
25
|
+
severity: "error",
|
|
26
|
+
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.`,
|
|
27
|
+
entity: policies.source,
|
|
28
|
+
lexicon: policies.lexicon,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return diagnostics;
|
|
33
|
+
},
|
|
34
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { dogwoodPolicyFiles, scanWindowlessOperators } from "../../dogwood/scan.js";
|
|
2
|
+
export const dwdc012 = {
|
|
3
|
+
id: "DWDC012",
|
|
4
|
+
description: "A dogwood formerly/previous/since operator carries its mandatory within window",
|
|
5
|
+
check(ctx) {
|
|
6
|
+
const diagnostics = [];
|
|
7
|
+
for (const policies of dogwoodPolicyFiles(ctx)) {
|
|
8
|
+
const seen = new Set();
|
|
9
|
+
for (const operator of scanWindowlessOperators(policies.text)) {
|
|
10
|
+
if (seen.has(operator))
|
|
11
|
+
continue;
|
|
12
|
+
seen.add(operator);
|
|
13
|
+
diagnostics.push({
|
|
14
|
+
checkId: "DWDC012",
|
|
15
|
+
severity: "error",
|
|
16
|
+
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> …\`.`,
|
|
17
|
+
entity: policies.source,
|
|
18
|
+
lexicon: policies.lexicon,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return diagnostics;
|
|
23
|
+
},
|
|
24
|
+
};
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { embeddedAgentCorePolicyStatements } from "../../agentcore/scan.js";
|
|
2
|
+
import { dogwoodSchemaFiles, temporalRegions } from "../../dogwood/scan.js";
|
|
3
|
+
export const dwdc013 = {
|
|
4
|
+
id: "DWDC013",
|
|
5
|
+
description: "An embedded AgentCore temporal statement has its event schema emitted beside it",
|
|
6
|
+
check(ctx) {
|
|
7
|
+
if (dogwoodSchemaFiles(ctx).length > 0)
|
|
8
|
+
return [];
|
|
9
|
+
const diagnostics = [];
|
|
10
|
+
const seen = new Set();
|
|
11
|
+
for (const embedded of embeddedAgentCorePolicyStatements(ctx)) {
|
|
12
|
+
if (temporalRegions(embedded.statement).length === 0)
|
|
13
|
+
continue;
|
|
14
|
+
const named = embedded.logicalId ?? embedded.source;
|
|
15
|
+
const key = `${embedded.lexicon}:${embedded.source}:${named}`;
|
|
16
|
+
if (seen.has(key))
|
|
17
|
+
continue;
|
|
18
|
+
seen.add(key);
|
|
19
|
+
diagnostics.push({
|
|
20
|
+
checkId: "DWDC013",
|
|
21
|
+
severity: "warning",
|
|
22
|
+
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.`,
|
|
23
|
+
entity: named,
|
|
24
|
+
lexicon: embedded.lexicon,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
return diagnostics;
|
|
28
|
+
},
|
|
29
|
+
};
|