@dotzen/dotzen 0.0.1

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.
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.requiresApproval = exports.hasBlocking = void 0;
4
+ exports.reportExitCode = reportExitCode;
5
+ exports.renderTerminal = renderTerminal;
6
+ exports.renderJson = renderJson;
7
+ exports.renderError = renderError;
8
+ const vocabulary_1 = require("../vocabulary");
9
+ const assertNever = (x) => {
10
+ throw new Error(`unhandled: ${JSON.stringify(x)}`);
11
+ };
12
+ const byEffect = (r, e) => r.violations.filter((v) => v.effect === e);
13
+ // Tiny ANSI helper — no dependency (stay pure-JS, minimal deps).
14
+ const ANSI = {
15
+ red: '\x1b[31m',
16
+ green: '\x1b[32m',
17
+ yellow: '\x1b[33m',
18
+ magenta: '\x1b[35m',
19
+ bold: '\x1b[1m',
20
+ reset: '\x1b[0m',
21
+ };
22
+ const makePaint = (color) => (text, ...codes) => color ? codes.join('') + text + ANSI.reset : text;
23
+ /** A blocking violation fired (only Block fails the build). */
24
+ const hasBlocking = (r) => r.violations.some((v) => v.effect === vocabulary_1.Effect.Block);
25
+ exports.hasBlocking = hasBlocking;
26
+ /** A require_approval rule fired — the pipeline must pause for sign-off. */
27
+ const requiresApproval = (r) => r.violations.some((v) => v.effect === vocabulary_1.Effect.RequireApproval);
28
+ exports.requiresApproval = requiresApproval;
29
+ /**
30
+ * Only BLOCK violations fail the build (exit 1). Warnings and
31
+ * require_approval do not hard-fail — the pipeline proceeds (to a manual
32
+ * approval gate when approval is required). See doc 04.
33
+ */
34
+ function reportExitCode(report) {
35
+ return (0, exports.hasBlocking)(report) ? 1 : 0;
36
+ }
37
+ function renderTerminal(report, opts = {}) {
38
+ const paint = makePaint(opts.color ?? false);
39
+ const lines = [];
40
+ const cne = report.couldNotEvaluate.length;
41
+ const section = (title, marker, tone, vs) => {
42
+ if (vs.length === 0)
43
+ return;
44
+ lines.push(paint(`── ${title.toUpperCase()} ──`, tone, ANSI.bold));
45
+ for (const v of vs) {
46
+ lines.push(`${paint(marker, tone)} ${v.resource} (${v.file}:${v.line})`);
47
+ lines.push(` ${v.message}`);
48
+ if (v.rationale)
49
+ lines.push(` ↳ ${v.rationale}`);
50
+ if (v.approvers?.length)
51
+ lines.push(` approvers: ${v.approvers.join(', ')}`);
52
+ }
53
+ };
54
+ // Most severe first.
55
+ section('blocking', '✗', ANSI.red, byEffect(report, vocabulary_1.Effect.Block));
56
+ section('approval required', '⏸', ANSI.yellow, byEffect(report, vocabulary_1.Effect.RequireApproval));
57
+ section('warnings', '‼', ANSI.yellow, byEffect(report, vocabulary_1.Effect.Warn));
58
+ if (report.couldNotEvaluate.length > 0) {
59
+ lines.push(paint('── COULD NOT EVALUATE ──', ANSI.magenta, ANSI.bold));
60
+ for (const u of report.couldNotEvaluate) {
61
+ lines.push(`${paint('?', ANSI.magenta)} ${u.resource} (${u.file}:${u.line}): ${u.reason} (${u.ruleId})`);
62
+ }
63
+ }
64
+ lines.push('');
65
+ if (report.violations.length === 0) {
66
+ lines.push(`${paint('✓ passed', ANSI.green)} (${report.passed} checks, ${paint(`${cne} could not be evaluated`, ANSI.magenta)})`);
67
+ }
68
+ else {
69
+ const count = `${report.violations.length} violation(s)`;
70
+ const vmark = (0, exports.hasBlocking)(report)
71
+ ? paint(`✗ ${count}`, ANSI.red)
72
+ : paint(`⚠ ${count}`, ANSI.yellow);
73
+ lines.push(`${vmark}, ${paint(`${report.passed} passed`, ANSI.green)}, ${paint(`${cne} could not be evaluated`, ANSI.magenta)}`);
74
+ if ((0, exports.requiresApproval)(report))
75
+ lines.push(paint('⏸ approval required before apply (DOTZEN_REQUIRES_APPROVAL)', ANSI.yellow));
76
+ }
77
+ return lines.join('\n');
78
+ }
79
+ function renderJson(report) {
80
+ return JSON.stringify({ ...report, requiresApproval: (0, exports.requiresApproval)(report) }, null, 2);
81
+ }
82
+ /** Exhaustive over DotzenError.kind (doc 06). */
83
+ function renderError(error) {
84
+ switch (error.kind) {
85
+ case 'ConfigNotFound':
86
+ return `✗ dotzen config not found: ${error.path}`;
87
+ case 'VersionMismatch':
88
+ return [
89
+ `✗ dotzen version mismatch`,
90
+ ` required: ${error.required} (from dotzen.json)`,
91
+ ` running: ${error.running}`,
92
+ ``,
93
+ ` run: npx @dotzen/dotzen@${error.required} check`,
94
+ ].join('\n');
95
+ case 'SpecLoadFailed':
96
+ return `✗ could not load spec ${error.path}: ${error.detail}`;
97
+ case 'SpecInvalid':
98
+ return [
99
+ `✗ spec has invalid rules:`,
100
+ ...error.errors.map((e) => ` rule ${e.ruleIndex + 1}: ${e.problem}`),
101
+ ].join('\n');
102
+ case 'PathNotFound':
103
+ return `✗ terraform path not found: ${error.path}`;
104
+ case 'ParseFailed':
105
+ return `✗ failed to parse ${error.file}: ${error.detail}`;
106
+ default:
107
+ return assertNever(error);
108
+ }
109
+ }
@@ -0,0 +1,27 @@
1
+ /** The failure track: operational failures only (doc 06, Rule 1). */
2
+ export interface RuleValidationError {
3
+ readonly ruleIndex: number;
4
+ readonly problem: string;
5
+ }
6
+ export type DotzenError = {
7
+ readonly kind: 'ConfigNotFound';
8
+ readonly path: string;
9
+ } | {
10
+ readonly kind: 'VersionMismatch';
11
+ readonly required: string;
12
+ readonly running: string;
13
+ } | {
14
+ readonly kind: 'SpecLoadFailed';
15
+ readonly path: string;
16
+ readonly detail: string;
17
+ } | {
18
+ readonly kind: 'SpecInvalid';
19
+ readonly errors: RuleValidationError[];
20
+ } | {
21
+ readonly kind: 'PathNotFound';
22
+ readonly path: string;
23
+ } | {
24
+ readonly kind: 'ParseFailed';
25
+ readonly file: string;
26
+ readonly detail: string;
27
+ };
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ /** The failure track: operational failures only (doc 06, Rule 1). */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,22 @@
1
+ /**
2
+ * Railway Oriented Programming core (doc 06). Hand-rolled, zero-dep.
3
+ * Operational failures ride the Err track; violations never do.
4
+ */
5
+ export type Result<T, E> = {
6
+ readonly ok: true;
7
+ readonly value: T;
8
+ } | {
9
+ readonly ok: false;
10
+ readonly error: E;
11
+ };
12
+ export declare const ok: <T>(value: T) => Result<T, never>;
13
+ export declare const err: <E>(error: E) => Result<never, E>;
14
+ /** Railway combinator: transform the success value, pass errors through. */
15
+ export declare function map<T, U, E>(r: Result<T, E>, f: (t: T) => U): Result<U, E>;
16
+ /** Railway combinator: chain a fallible step, short-circuit on error. */
17
+ export declare function andThen<T, U, E>(r: Result<T, E>, f: (t: T) => Result<U, E>): Result<U, E>;
18
+ /**
19
+ * Fold combinator (NOT a railway): run all, accumulate every error.
20
+ * Used by spec loading so every invalid rule is reported at once.
21
+ */
22
+ export declare function combineWithAllErrors<T, E>(results: Result<T, E>[]): Result<T[], E[]>;
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ /**
3
+ * Railway Oriented Programming core (doc 06). Hand-rolled, zero-dep.
4
+ * Operational failures ride the Err track; violations never do.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.err = exports.ok = void 0;
8
+ exports.map = map;
9
+ exports.andThen = andThen;
10
+ exports.combineWithAllErrors = combineWithAllErrors;
11
+ const ok = (value) => ({ ok: true, value });
12
+ exports.ok = ok;
13
+ const err = (error) => ({ ok: false, error });
14
+ exports.err = err;
15
+ /** Railway combinator: transform the success value, pass errors through. */
16
+ function map(r, f) {
17
+ return r.ok ? (0, exports.ok)(f(r.value)) : r;
18
+ }
19
+ /** Railway combinator: chain a fallible step, short-circuit on error. */
20
+ function andThen(r, f) {
21
+ return r.ok ? f(r.value) : r;
22
+ }
23
+ /**
24
+ * Fold combinator (NOT a railway): run all, accumulate every error.
25
+ * Used by spec loading so every invalid rule is reported at once.
26
+ */
27
+ function combineWithAllErrors(results) {
28
+ const values = [];
29
+ const errors = [];
30
+ for (const r of results) {
31
+ if (r.ok)
32
+ values.push(r.value);
33
+ else
34
+ errors.push(r.error);
35
+ }
36
+ return errors.length > 0 ? (0, exports.err)(errors) : (0, exports.ok)(values);
37
+ }
@@ -0,0 +1,14 @@
1
+ import { Result } from '../result/result';
2
+ import { DotzenError } from '../result/errors';
3
+ import { RuleBuilder, Rule } from './rule';
4
+ /**
5
+ * Load `.zen/spec.ts` via a pure-JS runtime TypeScript loader (jiti) —
6
+ * decided in doc 06 §"Spec loading". This is the isolated seam; swapping
7
+ * the loader touches only this function.
8
+ */
9
+ export declare function importSpecModule(specPath: string): Promise<Result<RuleBuilder[], DotzenError>>;
10
+ /**
11
+ * Validate all rules, ACCUMULATING every problem (doc 06, Rule 3) rather
12
+ * than failing on the first invalid rule.
13
+ */
14
+ export declare function loadSpec(builders: RuleBuilder[]): Result<Rule[], DotzenError>;
@@ -0,0 +1,78 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.importSpecModule = importSpecModule;
37
+ exports.loadSpec = loadSpec;
38
+ const fs = __importStar(require("fs"));
39
+ const jiti_1 = require("jiti");
40
+ const result_1 = require("../result/result");
41
+ /**
42
+ * Load `.zen/spec.ts` via a pure-JS runtime TypeScript loader (jiti) —
43
+ * decided in doc 06 §"Spec loading". This is the isolated seam; swapping
44
+ * the loader touches only this function.
45
+ */
46
+ async function importSpecModule(specPath) {
47
+ if (!fs.existsSync(specPath))
48
+ return (0, result_1.err)({ kind: 'ConfigNotFound', path: specPath });
49
+ try {
50
+ const jiti = (0, jiti_1.createJiti)(__filename);
51
+ const mod = (await jiti.import(specPath));
52
+ const spec = mod.spec;
53
+ if (!Array.isArray(spec))
54
+ return (0, result_1.err)({
55
+ kind: 'SpecLoadFailed',
56
+ path: specPath,
57
+ detail: 'spec.ts must export a `spec` array of rules',
58
+ });
59
+ return (0, result_1.ok)(spec);
60
+ }
61
+ catch (e) {
62
+ return (0, result_1.err)({
63
+ kind: 'SpecLoadFailed',
64
+ path: specPath,
65
+ detail: e instanceof Error ? e.message : String(e),
66
+ });
67
+ }
68
+ }
69
+ /**
70
+ * Validate all rules, ACCUMULATING every problem (doc 06, Rule 3) rather
71
+ * than failing on the first invalid rule.
72
+ */
73
+ function loadSpec(builders) {
74
+ const validated = (0, result_1.combineWithAllErrors)(builders.map((b, i) => b.validate(i)));
75
+ if (validated.ok)
76
+ return (0, result_1.ok)(validated.value);
77
+ return (0, result_1.err)({ kind: 'SpecInvalid', errors: validated.error.flat() });
78
+ }
@@ -0,0 +1,161 @@
1
+ import { Port, Cidr, Effect, Tag, Acl, Environment, Approver, Block, AnyResource, AnyAttribute } from '../vocabulary';
2
+ import { Result } from '../result/result';
3
+ import { RuleValidationError } from '../result/errors';
4
+ export type ResourceTarget = {
5
+ readonly kind: 'resource';
6
+ readonly types: AnyResource[];
7
+ } | {
8
+ readonly kind: 'all';
9
+ };
10
+ /** Discriminated union so the engine dispatches one evaluator per kind. */
11
+ export type Condition = {
12
+ readonly kind: 'denyIngress';
13
+ readonly ports: Port[];
14
+ readonly from: Cidr[];
15
+ } | {
16
+ readonly kind: 'denyEgress';
17
+ readonly ports: Port[];
18
+ readonly from: Cidr[];
19
+ } | {
20
+ readonly kind: 'mustHaveTags';
21
+ readonly tags: Tag[];
22
+ } | {
23
+ readonly kind: 'mustBeTrue';
24
+ readonly attrs: AnyAttribute[];
25
+ } | {
26
+ readonly kind: 'mustBeFalse';
27
+ readonly attrs: AnyAttribute[];
28
+ } | {
29
+ readonly kind: 'mustBeSet';
30
+ readonly attrs: AnyAttribute[];
31
+ } | {
32
+ readonly kind: 'denyWhenTrue';
33
+ readonly attrs: AnyAttribute[];
34
+ } | {
35
+ readonly kind: 'denyAcl';
36
+ readonly acls: Acl[];
37
+ } | {
38
+ readonly kind: 'mustEqual';
39
+ readonly attr: AnyAttribute;
40
+ readonly value: string;
41
+ } | {
42
+ readonly kind: 'mustBeAtLeast';
43
+ readonly attr: AnyAttribute;
44
+ readonly min: number;
45
+ } | {
46
+ readonly kind: 'mustBeAtMost';
47
+ readonly attr: AnyAttribute;
48
+ readonly max: number;
49
+ } | {
50
+ readonly kind: 'denyIamWildcard';
51
+ } | {
52
+ readonly kind: 'listContains';
53
+ readonly attr: AnyAttribute;
54
+ readonly values: string[];
55
+ } | {
56
+ readonly kind: 'listMustInclude';
57
+ readonly attr: AnyAttribute;
58
+ readonly values: string[];
59
+ } | {
60
+ readonly kind: 'denyValue';
61
+ readonly attr: AnyAttribute;
62
+ readonly values: string[];
63
+ } | {
64
+ readonly kind: 'mustBeOneOf';
65
+ readonly attr: AnyAttribute;
66
+ readonly values: string[];
67
+ } | {
68
+ readonly kind: 'denyPlaintextListener';
69
+ } | {
70
+ readonly kind: 'denyPrivilegedContainers';
71
+ } | {
72
+ readonly kind: 'denyLiteral';
73
+ readonly attrs: AnyAttribute[];
74
+ } | {
75
+ readonly kind: 'mustHaveAssociated';
76
+ readonly childType: AnyResource;
77
+ readonly via: AnyAttribute;
78
+ } | {
79
+ readonly kind: 'mustHaveBlock';
80
+ readonly block: Block;
81
+ } | {
82
+ readonly kind: 'denyBlockPresence';
83
+ readonly block: Block;
84
+ };
85
+ export interface Rule {
86
+ readonly id: string;
87
+ readonly target: ResourceTarget;
88
+ /** If set, the rule applies only to resources in this environment. */
89
+ readonly environment?: Environment;
90
+ readonly conditions: Condition[];
91
+ readonly effect: Effect;
92
+ readonly message: string;
93
+ readonly rationale?: string;
94
+ /** For require_approval rules: who must sign off. */
95
+ readonly approvers?: Approver[];
96
+ }
97
+ /**
98
+ * The authored surface (doc 02). The builder IS the rule object;
99
+ * `validate()` (called by the engine on load) returns a normalized Rule
100
+ * on success or ACCUMULATES problems on failure (doc 06, ROP form).
101
+ */
102
+ export declare class RuleBuilder {
103
+ private _target?;
104
+ private _environment?;
105
+ private _conditions;
106
+ private _effect;
107
+ private _message?;
108
+ private _rationale?;
109
+ private _approvers;
110
+ resource(...types: AnyResource[]): this;
111
+ allResources(): this;
112
+ environment(env: Environment): this;
113
+ denyIngress(...ports: Port[]): this;
114
+ denyEgress(...ports: Port[]): this;
115
+ mustHaveTags(...tags: Tag[]): this;
116
+ mustBeTrue(...attrs: AnyAttribute[]): this;
117
+ /** AnyAttribute must be explicitly false; absent counts as a violation
118
+ * (use for attributes whose insecure AWS default is `true`). */
119
+ mustBeFalse(...attrs: AnyAttribute[]): this;
120
+ /** AnyAttribute must be present (any value); absent is the violation. */
121
+ mustBeSet(...attrs: AnyAttribute[]): this;
122
+ denyWhenTrue(...attrs: AnyAttribute[]): this;
123
+ denyAcl(...acls: Acl[]): this;
124
+ mustEqual(attr: AnyAttribute, value: string): this;
125
+ mustBeAtLeast(attr: AnyAttribute, min: number): this;
126
+ /** Numeric attribute must be <= max; absent/above is the violation. */
127
+ mustBeAtMost(attr: AnyAttribute, max: number): this;
128
+ /** Flag Allow statements that grant `Action: "*"` (full privileges). */
129
+ denyIamWildcard(): this;
130
+ /** Flag a list attribute that CONTAINS any of `values` (e.g. a public CIDR). */
131
+ listContains(attr: AnyAttribute, ...values: string[]): this;
132
+ /** Require a list attribute to INCLUDE all of `values` (e.g. audit log types). */
133
+ listMustInclude(attr: AnyAttribute, ...values: string[]): this;
134
+ /** Flag a scalar attribute whose value is one of `values` (e.g. a weak TLS policy). */
135
+ denyValue(attr: AnyAttribute, ...values: string[]): this;
136
+ /** Require a scalar attribute's value to be one of `values` (allowlist);
137
+ * absent or any other value is the violation. */
138
+ mustBeOneOf(attr: AnyAttribute, ...values: string[]): this;
139
+ /** Flag a plaintext listener (HTTP/TCP) unless it redirects (default_action). */
140
+ denyPlaintextListener(): this;
141
+ /** Flag an ECS task definition with any privileged container. */
142
+ denyPrivilegedContainers(): this;
143
+ /** Flag a hardcoded literal where a reference is expected (e.g. a secret).
144
+ * A `var`/`data` reference passes; a literal value is the violation. */
145
+ denyLiteral(...attrs: AnyAttribute[]): this;
146
+ /** Require a separate `childType` resource to reference this one via `via`
147
+ * (e.g. an S3 bucket must have a matching server-side-encryption config). */
148
+ mustHaveAssociated(childType: AnyResource, via: AnyAttribute): this;
149
+ /** Require this resource to declare a given nested block (e.g. EKS
150
+ * `encryption_config`). */
151
+ mustHaveBlock(block: Block): this;
152
+ /** Flag this resource if it declares a given nested block (e.g. a GCP
153
+ * instance `network_interface.access_config` = a public IP). */
154
+ denyBlockPresence(block: Block): this;
155
+ onViolation(effect: Effect): this;
156
+ approvers(...names: Approver[]): this;
157
+ message(msg: string): this;
158
+ rationale(text: string): this;
159
+ validate(index: number): Result<Rule, RuleValidationError[]>;
160
+ }
161
+ export declare const rule: () => RuleBuilder;
@@ -0,0 +1,190 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.rule = exports.RuleBuilder = void 0;
4
+ const vocabulary_1 = require("../vocabulary");
5
+ const result_1 = require("../result/result");
6
+ /**
7
+ * The authored surface (doc 02). The builder IS the rule object;
8
+ * `validate()` (called by the engine on load) returns a normalized Rule
9
+ * on success or ACCUMULATES problems on failure (doc 06, ROP form).
10
+ */
11
+ class RuleBuilder {
12
+ _target;
13
+ _environment;
14
+ _conditions = [];
15
+ _effect = vocabulary_1.Effect.Block;
16
+ _message;
17
+ _rationale;
18
+ _approvers = [];
19
+ resource(...types) {
20
+ this._target = { kind: 'resource', types };
21
+ return this;
22
+ }
23
+ allResources() {
24
+ this._target = { kind: 'all' };
25
+ return this;
26
+ }
27
+ environment(env) {
28
+ this._environment = env;
29
+ return this;
30
+ }
31
+ denyIngress(...ports) {
32
+ this._conditions.push({
33
+ kind: 'denyIngress',
34
+ ports,
35
+ from: [vocabulary_1.Cidr.Internet, vocabulary_1.Cidr.InternetV6],
36
+ });
37
+ return this;
38
+ }
39
+ denyEgress(...ports) {
40
+ this._conditions.push({
41
+ kind: 'denyEgress',
42
+ ports,
43
+ from: [vocabulary_1.Cidr.Internet, vocabulary_1.Cidr.InternetV6],
44
+ });
45
+ return this;
46
+ }
47
+ mustHaveTags(...tags) {
48
+ this._conditions.push({ kind: 'mustHaveTags', tags });
49
+ return this;
50
+ }
51
+ mustBeTrue(...attrs) {
52
+ this._conditions.push({ kind: 'mustBeTrue', attrs });
53
+ return this;
54
+ }
55
+ /** AnyAttribute must be explicitly false; absent counts as a violation
56
+ * (use for attributes whose insecure AWS default is `true`). */
57
+ mustBeFalse(...attrs) {
58
+ this._conditions.push({ kind: 'mustBeFalse', attrs });
59
+ return this;
60
+ }
61
+ /** AnyAttribute must be present (any value); absent is the violation. */
62
+ mustBeSet(...attrs) {
63
+ this._conditions.push({ kind: 'mustBeSet', attrs });
64
+ return this;
65
+ }
66
+ denyWhenTrue(...attrs) {
67
+ this._conditions.push({ kind: 'denyWhenTrue', attrs });
68
+ return this;
69
+ }
70
+ denyAcl(...acls) {
71
+ this._conditions.push({ kind: 'denyAcl', acls });
72
+ return this;
73
+ }
74
+ mustEqual(attr, value) {
75
+ this._conditions.push({ kind: 'mustEqual', attr, value });
76
+ return this;
77
+ }
78
+ mustBeAtLeast(attr, min) {
79
+ this._conditions.push({ kind: 'mustBeAtLeast', attr, min });
80
+ return this;
81
+ }
82
+ /** Numeric attribute must be <= max; absent/above is the violation. */
83
+ mustBeAtMost(attr, max) {
84
+ this._conditions.push({ kind: 'mustBeAtMost', attr, max });
85
+ return this;
86
+ }
87
+ /** Flag Allow statements that grant `Action: "*"` (full privileges). */
88
+ denyIamWildcard() {
89
+ this._conditions.push({ kind: 'denyIamWildcard' });
90
+ return this;
91
+ }
92
+ /** Flag a list attribute that CONTAINS any of `values` (e.g. a public CIDR). */
93
+ listContains(attr, ...values) {
94
+ this._conditions.push({ kind: 'listContains', attr, values });
95
+ return this;
96
+ }
97
+ /** Require a list attribute to INCLUDE all of `values` (e.g. audit log types). */
98
+ listMustInclude(attr, ...values) {
99
+ this._conditions.push({ kind: 'listMustInclude', attr, values });
100
+ return this;
101
+ }
102
+ /** Flag a scalar attribute whose value is one of `values` (e.g. a weak TLS policy). */
103
+ denyValue(attr, ...values) {
104
+ this._conditions.push({ kind: 'denyValue', attr, values });
105
+ return this;
106
+ }
107
+ /** Require a scalar attribute's value to be one of `values` (allowlist);
108
+ * absent or any other value is the violation. */
109
+ mustBeOneOf(attr, ...values) {
110
+ this._conditions.push({ kind: 'mustBeOneOf', attr, values });
111
+ return this;
112
+ }
113
+ /** Flag a plaintext listener (HTTP/TCP) unless it redirects (default_action). */
114
+ denyPlaintextListener() {
115
+ this._conditions.push({ kind: 'denyPlaintextListener' });
116
+ return this;
117
+ }
118
+ /** Flag an ECS task definition with any privileged container. */
119
+ denyPrivilegedContainers() {
120
+ this._conditions.push({ kind: 'denyPrivilegedContainers' });
121
+ return this;
122
+ }
123
+ /** Flag a hardcoded literal where a reference is expected (e.g. a secret).
124
+ * A `var`/`data` reference passes; a literal value is the violation. */
125
+ denyLiteral(...attrs) {
126
+ this._conditions.push({ kind: 'denyLiteral', attrs });
127
+ return this;
128
+ }
129
+ /** Require a separate `childType` resource to reference this one via `via`
130
+ * (e.g. an S3 bucket must have a matching server-side-encryption config). */
131
+ mustHaveAssociated(childType, via) {
132
+ this._conditions.push({ kind: 'mustHaveAssociated', childType, via });
133
+ return this;
134
+ }
135
+ /** Require this resource to declare a given nested block (e.g. EKS
136
+ * `encryption_config`). */
137
+ mustHaveBlock(block) {
138
+ this._conditions.push({ kind: 'mustHaveBlock', block });
139
+ return this;
140
+ }
141
+ /** Flag this resource if it declares a given nested block (e.g. a GCP
142
+ * instance `network_interface.access_config` = a public IP). */
143
+ denyBlockPresence(block) {
144
+ this._conditions.push({ kind: 'denyBlockPresence', block });
145
+ return this;
146
+ }
147
+ onViolation(effect) {
148
+ this._effect = effect;
149
+ return this;
150
+ }
151
+ approvers(...names) {
152
+ this._approvers = names;
153
+ return this;
154
+ }
155
+ message(msg) {
156
+ this._message = msg;
157
+ return this;
158
+ }
159
+ rationale(text) {
160
+ this._rationale = text;
161
+ return this;
162
+ }
163
+ validate(index) {
164
+ const problems = [];
165
+ const fail = (problem) => problems.push({ ruleIndex: index, problem });
166
+ const hasTarget = this._target &&
167
+ (this._target.kind === 'all' || this._target.types.length > 0);
168
+ if (!this._message)
169
+ fail('missing .message()');
170
+ if (!hasTarget)
171
+ fail('missing .resource() or .allResources()');
172
+ if (this._conditions.length === 0)
173
+ fail('no conditions');
174
+ if (problems.length > 0)
175
+ return (0, result_1.err)(problems);
176
+ return (0, result_1.ok)({
177
+ id: `rule-${index + 1}`,
178
+ target: this._target,
179
+ environment: this._environment,
180
+ conditions: this._conditions,
181
+ effect: this._effect,
182
+ message: this._message,
183
+ rationale: this._rationale,
184
+ approvers: this._approvers.length > 0 ? this._approvers : undefined,
185
+ });
186
+ }
187
+ }
188
+ exports.RuleBuilder = RuleBuilder;
189
+ const rule = () => new RuleBuilder();
190
+ exports.rule = rule;
@@ -0,0 +1,29 @@
1
+ import { Result } from '../result/result';
2
+ import { DotzenError } from '../result/errors';
3
+ /**
4
+ * A Terraform root: a bare path, or a path with a declared `environment`
5
+ * so `.environment(X)` rule scoping is folder-driven (not tag-dependent).
6
+ */
7
+ export type TerraformRoot = string | {
8
+ readonly path: string;
9
+ readonly environment?: string;
10
+ };
11
+ export interface DotzenConfig {
12
+ readonly version?: string;
13
+ readonly spec: string;
14
+ /** One Terraform root, or several (each evaluated with its own scope). */
15
+ readonly terraform: TerraformRoot | TerraformRoot[];
16
+ }
17
+ /** Resolved config plus the directory it was found in (for relative paths). */
18
+ export interface LoadedConfig {
19
+ readonly config: DotzenConfig;
20
+ readonly baseDir: string;
21
+ }
22
+ export declare function readDotzenJson(cwd: string): Result<LoadedConfig, DotzenError>;
23
+ /**
24
+ * The first thing the CLI does. Refuse to run on a version mismatch
25
+ * (doc 03 / engine-dev skill). No pin configured => proceed.
26
+ */
27
+ export declare function enforceVersion<T extends {
28
+ version?: string;
29
+ }>(config: T, running: string): Result<T, DotzenError>;