flecto 2.1.0 → 3.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.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,438 @@ The format is based on [Keep a Changelog], and this project adheres to
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [3.0.1] - 2026-08-07
11
+
12
+ ### Security
13
+
14
+ - **Policy plugins declared in `.flectorc` are no longer loaded**
15
+ ([GHSA-wq8m-fc3q-8m5x], critical). A pull request that added a `.flectorc`
16
+ with a `plugins` entry achieved **arbitrary code execution on the CI runner** —
17
+ `flecto ci` is what teams run on pull requests, and it honoured the attacker's
18
+ config with no opt-in, no allowlist, and no path containment. The attacker's
19
+ code ran with whatever the workflow exposed, including `GITHUB_TOKEN`, and the
20
+ path was not contained, so `../../../../tmp/x.mjs` loaded a module from
21
+ anywhere on disk.
22
+
23
+ Plugins now load only from an explicit `--plugins` flag. If a config file is
24
+ genuinely trusted, set `FLECTO_ALLOW_RC_PLUGINS=1`; even then an rc-declared
25
+ plugin must live inside the working directory. Flecto **fails loudly** rather
26
+ than skipping the plugin silently, because a policy plugin that stopped running
27
+ without saying so would quietly weaken a gate the operator believes is
28
+ enforced.
29
+
30
+ Policy *packs* are declarative and were never affected. `--plugins` is
31
+ unchanged, including paths outside the project, since the flag is operator
32
+ intent rather than attacker input.
33
+
34
+ **If you run Flecto on untrusted pull requests, upgrade.** If you rely on
35
+ `plugins` in `.flectorc`, move it to `--plugins` or set the opt-in.
36
+
37
+ The trust boundary is now documented in [plugin authoring](docs/plugins.md);
38
+ it previously was not stated anywhere.
39
+
40
+
41
+ ## [3.0.0] - 2026-08-06
42
+
43
+ ### Migration notes
44
+
45
+ Flecto 3.0.0 is additive in surface — no command, flag, or envelope field was
46
+ removed, exit codes are unchanged, and `schema_version` is still `2.0`. Two
47
+ behavior changes can turn a green 2.1.0 pipeline red, so read these first.
48
+
49
+ **1. The `default` policy pack catches more.** Value-pattern secret detection
50
+ and the SOPS decryption rules were added to `default`, so a credential-shaped
51
+ value under an innocuous key name — or a secret committed in the clear — is now
52
+ an `error`. A pipeline using `--fail-on policy` can fail on config that passed
53
+ in 2.1.0:
54
+
55
+ ```
56
+ flecto ci config.yaml --fail-on policy,error
57
+ # 2.1.0 -> exit 0 3.0.0 -> exit 1
58
+ ```
59
+
60
+ That is the intended behavior, but it is worth a dry run before upgrading CI.
61
+ To keep the 2.1.0 rule set while you triage, name the packs explicitly and
62
+ silence the new rules with `severityRemap`:
63
+
64
+ ```json
65
+ { "profiles": { "ci": { "severityRemap": {
66
+ "secret-value-detected": "off",
67
+ "sops-file-decrypted": "off",
68
+ "sops-value-decrypted": "off"
69
+ } } } }
70
+ ```
71
+
72
+ **2. Unknown `--fail-on` triggers are now an error.** A typo previously matched
73
+ nothing and the run exited `0`, so the gate was silently absent:
74
+
75
+ ```
76
+ flecto ci config.yaml --fail-on "polciy,eror"
77
+ # 2.1.0 -> exit 0, ignored 3.0.0 -> exit 1, "unknown triggers: polciy, eror"
78
+ ```
79
+
80
+ Any pipeline carrying a typo will go red on upgrade. That is the bug being
81
+ fixed — those runs were never actually gated — but the failure is new.
82
+
83
+ **Also worth knowing, unlikely to break a build:**
84
+
85
+ - **Multi-document YAML now parses** instead of failing the file. Paths inside
86
+ such a file are prefixed with the document identity (`Deployment/prod/api.…`),
87
+ so `--ignore` entries and custom pack path regexes written against
88
+ single-document paths will not match. Single-document files are unchanged.
89
+ - **Encrypted files no longer emit ciphertext to machine consumers.** 2.1.0 put
90
+ `ENC[AES256_GCM,data:…]` in the diff; 3.0.0 emits sentinels. Anything parsing
91
+ the JSON/NDJSON envelope for SOPS files needs updating.
92
+ - **`flecto init` no longer overwrites an existing config.** 2.1.0 silently
93
+ regenerated `.flectorc.json`, destroying edits. Both still exit `0`, so a
94
+ script relying on regeneration now gets a no-op.
95
+ - **`--mask-secrets` masks more than before** — value-shaped detection and
96
+ nested values, not only top-level sensitive key names.
97
+
98
+ ### Added
99
+
100
+ - A test that every runtime dependency's `engines.node` is satisfiable by the
101
+ Node version Flecto itself declares, plus a check that the CI matrix actually
102
+ exercises that floor. This class of bug has now happened twice ([#22], and
103
+ chalk 6 requiring Node >=22 in [#104]) and CI could not catch it: `engines` is
104
+ advisory, so the Node 20 job passes while npm warns users with `EBADENGINE`.
105
+ The check reads each manifest off disk rather than through `require()`,
106
+ because a package whose `exports` map hides `./package.json` — chalk 6 is
107
+ exactly that — would otherwise be skipped silently. `chalk` majors are held in
108
+ Dependabot alongside `commander` and `js-yaml`. ([#104])
109
+ - Pre-merge review of rendered Kubernetes manifests, and a `kubernetes` policy
110
+ pack to gate it. ArgoCD, Flux, and `helm diff` compare a cluster to the
111
+ repository; this compares the manifests a pull request *would* produce against
112
+ the ones the merge target produces, before `helm upgrade` runs. The workflow
113
+ needs no new command and no new dependency: render both sides to plain
114
+ multi-document YAML with whatever you already use — `helm template`,
115
+ `kustomize build`, `kubectl kustomize`, `jsonnet`, `cdk8s` — and diff them with
116
+ `flecto compare base.yaml head.yaml --policies kubernetes`. Repositories that
117
+ commit their rendered output can use `flecto ci manifests/prod.yaml
118
+ --snapshot-ref origin/main` instead and render once. **Flecto never invokes
119
+ `helm` or `kustomize`**; neither is a dependency and neither has to exist on
120
+ the runner, which is what keeps the renderer your choice. The pack carries ten
121
+ rules for changes that are risky at review time: `privileged`, host
122
+ namespaces, weakened `runAsNonRoot`, `allowPrivilegeEscalation`, `SYS_ADMIN` /
123
+ `NET_ADMIN` / `ALL` capabilities, images that resolve to `:latest`,
124
+ `imagePullPolicy` moving to `Always`, replica jumps, removed resource limits,
125
+ and Services becoming `LoadBalancer` or `NodePort`. Thresholds are tuned so
126
+ routine work stays quiet — a replica jump needs both a 3× multiple and an
127
+ increase of at least 3, so `1 → 2` does not fire. Policy packs also gained an
128
+ optional pack-level `expandSubtrees`, which expands added and removed subtrees
129
+ into the leaf changes they imply before rules run; without it a brand-new
130
+ `Service` document is a single change carrying the whole manifest, and a rule
131
+ anchored at `spec.type` never sees inside it. It is opt-in per pack and off by
132
+ default, so every existing pack behaves exactly as before. ([#76])
133
+ - SOPS- and age-aware diffing, structural and **without ever decrypting**.
134
+ Encrypted files were previously the ones Flecto helped with least: skipped, or
135
+ read as ordinary YAML with ciphertext blobs filling the diff. They are now
136
+ detected from their **contents** — a SOPS metadata block (a `sops` map with a
137
+ version plus a MAC, a modification stamp, or a key group; also the flat
138
+ `sops_*` form used for dotenv and INI), or a recognized ciphertext container
139
+ (`ENC[AES256_GCM,…]`, an armored age blob, an armored PGP message). Filenames
140
+ are only a hint: teams commit fully encrypted `values.prod.yaml`, and
141
+ `.sops.yaml` is a *plaintext* creation-rules config. A config that merely pins
142
+ `sops.version` is not mistaken for an encrypted file. `.age` files, and any
143
+ file that is one armored blob, are now supported as a single opaque value.
144
+ Every ciphertext-bearing value is replaced **in the parser** with an opaque
145
+ `<encrypted:SCHEME:DIGEST>` sentinel, so no diff, snapshot, webhook payload,
146
+ PR comment, or HTML report can carry ciphertext — there is no code path that
147
+ produces any, with or without `--mask-secrets`. Human output collapses it
148
+ further, to `~ db.password: <encrypted value changed>`. What you get instead
149
+ is the structure: keys added and removed, which encrypted values moved, the
150
+ `sops` metadata block, and — the useful part — the recipient list. Public
151
+ identifiers stay visible (age recipient, PGP fingerprint, KMS ARN) while the
152
+ data key sealed to each is redacted, and the key groups are re-keyed by
153
+ recipient identity so a recipient inserted at the front reads as one addition
154
+ rather than "every recipient changed". Two synthetic paths carry what a
155
+ key-by-key walk cannot express: `<encryption>` when a file gains or loses
156
+ encryption, and `<encryption.mac>` when the MAC moves while every value it
157
+ covers stays put. Both respect `--ignore` like any other path. A value that
158
+ stopped being encrypted is reported as changed with the new value withheld —
159
+ the event is that it was exposed, and a CI log should not widen that. A new
160
+ built-in `sops` policy pack covers recipient added (`error`), recipient
161
+ removed (`warn`), a lone MAC change (`warn`), a file that became encrypted
162
+ (`info`), and `.sops.yaml` creation-rule recipient changes (`warn`); the
163
+ `default` pack gains the two that catch a secret committed in the clear,
164
+ `sops-file-decrypted` and `sops-value-decrypted`, both `error`. Flecto never
165
+ shells out to `sops`, `age`, or `gpg`, never reads a key file, agent socket,
166
+ or KMS credential, and has no flag that turns decryption on. Unencrypted files
167
+ are untouched: a tree with nothing to redact comes back from the encryption
168
+ pass as the same object, and a diff between two of them returns the very array
169
+ it always did. ([#77])
170
+ - A second bundled composite Action, `flecto-pr-risk`, that packages the pull
171
+ request risk comment as a one-line adoption: `uses:` it after
172
+ `actions/checkout` and the defaults do the rest (`format: pr-comment`,
173
+ posting on, `fail-on: policy,error`, secret masking on, the workflow token).
174
+ It resolves the baseline from the pull request instead of `HEAD~1`, which is
175
+ the wrong commit on a PR — `github.event.pull_request.base.sha`, refined to
176
+ the merge base with `HEAD` when the checkout carries enough history. A
177
+ missing base commit is fetched if it can be; when it still cannot be resolved
178
+ the job fails with a message naming `fetch-depth: 0`, rather than reporting
179
+ "no changes" and letting a risky edit through. Posting degrades instead of
180
+ breaking: a fork's read-only token, a missing `pull-requests: write`, or an
181
+ empty `github-token` produce a workflow warning and a report in the log,
182
+ never a failed check — the exit code stays with the diff and policy result.
183
+ `flecto-version` pins the CLI without forking the Action. The existing
184
+ `flecto-ci` Action is untouched, inputs and defaults included, and is now
185
+ covered by tests that parse both committed `action.yml` files. ([#74])
186
+ - `flecto report [files...]`: a static HTML drift report rendered from the local
187
+ snapshot history `flecto history` already reads, written to
188
+ `--output` (default `flecto-report.html`). The page carries a per-file
189
+ timeline — each snapshot with its UTC timestamp, the snapshot it is measured
190
+ against, its semantic changes, and the policy findings those changes produced
191
+ — plus a summary and every finding grouped by severity. `--limit`,
192
+ `--profile`, `--ignore`, `--policies`, `--plugins`, and the array-identity
193
+ flags resolve through the same effective-options path as every other command,
194
+ so a report matches what `flecto history` and `flecto watch --diff` report.
195
+ The file is **fully self-contained**: inline CSS, one small inline script for
196
+ filtering and collapsing, and nothing else — no fonts, no images, no CDN
197
+ scripts, no analytics, and no network access when it is opened. It follows the
198
+ viewer's light or dark theme, is responsive, and prints. Every config value,
199
+ path, and message is HTML-escaped, so a value containing markup renders as
200
+ text rather than as part of the page. `--mask-secrets` (flag or profile)
201
+ applies the same key-name and value-pattern redaction used elsewhere, and also
202
+ redacts policy messages that interpolate values — a report is a shareable
203
+ artifact, so a leak there is worse than one in a terminal. With no snapshots
204
+ it prints the same guidance `flecto history` does and writes no file. ([#75])
205
+ - A convention for distributing policy packs, plus `flecto policies add <name>`
206
+ to install one. A community pack is an npm package named `flecto-pack-<id>`
207
+ (or `@scope/flecto-pack-<id>`) with a `flecto-pack.json`, `flecto-pack.yaml`,
208
+ or `flecto-pack.yml` at its root — no build step, no entry point, no code. A
209
+ package that builds its pack elsewhere can point at it with a `"flecto"` field
210
+ in its package.json (`{ "pack": "dist/pack.json" }`, or the bare path).
211
+ `flecto policies add` takes either the pack id or the full package name,
212
+ resolves the already-installed package from `node_modules`, validates it with
213
+ the same validator that runs at evaluation time, and writes it to
214
+ `policies/<id>.json` so the existing resolution order picks it up unchanged. A
215
+ malformed third-party pack is rejected at add time rather than failing later
216
+ during evaluation, and an existing local pack is never overwritten without
217
+ `--force`. Nothing from the package is imported or executed: only the
218
+ declarative pack file is read, JavaScript shipped in a pack package is ignored
219
+ (and reported), and a `"flecto"` field pointing at a `.js` file is rejected.
220
+ Plugins, which do run code, are deliberately out of scope for this command.
221
+ `flecto policies list` now reports the originating npm package for packs
222
+ installed this way, tracked in `policies/.flecto-packs.json`; hand-written
223
+ local packs list exactly as before. ([#71])
224
+ - Value-pattern secret detection. Secrets are now found by what the value looks
225
+ like, not only by the key name: known token formats (AWS `AKIA…`/`ASIA…`,
226
+ GitHub `ghp_…`/`gho_…`/`ghu_…`/`ghs_…`/`ghr_…`, Slack `xox[abprs]-…`, Google
227
+ `AIza…`, Stripe `sk_live_…`/`rk_live_…`, JWTs, PEM private-key blocks, and
228
+ credentials embedded in a `scheme://user:password@host` URL) plus a
229
+ conservative high-entropy fallback for opaque strings. The same detection
230
+ drives `--mask-secrets` redaction and the new `secret-value-detected` rule in
231
+ the built-in `default` and `strict-prod` packs, so a credential under a boring
232
+ key such as `db.connstr` is both flagged and masked. Packs can use it directly
233
+ through the new `afterLooksSecret` / `beforeLooksSecret` predicates. Key-name
234
+ detection is unchanged. ([#66])
235
+ - Native Slack, Discord, and Microsoft Teams alert payloads:
236
+ `flecto watch --webhook-format <flecto|slack|discord|teams|auto>` (or
237
+ `webhookFormat` in `.flectorc`). The existing webhook path is reused as-is —
238
+ headers, `--webhook-timeout`, `--webhook-retries`, `--delivery-mode`, and
239
+ `--on-alert-failure` all behave identically; only the request body changes, so
240
+ no receiver of your own is needed. Slack gets Block Kit `blocks` with an
241
+ mrkdwn `text` fallback, Discord an embed colored by the highest policy
242
+ severity, Teams a MessageCard. Long change sets truncate to `… +N more`
243
+ within each service's documented limits (Slack 3000 chars per section, Discord
244
+ 4096 per embed description, Teams 28 KB per message). `auto` detects the
245
+ format from the webhook host (`hooks.slack.com`, `discord.com/api/webhooks`,
246
+ `*.office.com`) and is opt-in: the default remains `flecto`, which posts the
247
+ raw envelope byte-for-byte as before. `--mask-secrets-webhooks` applies to
248
+ chat payloads too. ([#68])
249
+ - `flecto ci --format pr-comment`: a markdown risk summary for pull requests —
250
+ change counts, policy findings grouped by severity with file and path, and the
251
+ per-file change list, collapsed into a `<details>` block past ten changes.
252
+ The body opens with a hidden `<!-- flecto:pr-comment -->` marker, so posting
253
+ updates the one comment Flecto already left instead of adding a new one per
254
+ push; an unchanged report skips the write entirely. Rendering to stdout is the
255
+ default and never touches the network. Posting requires **both** the explicit
256
+ `--pr-comment-post` opt-in and a complete GitHub pull request context
257
+ (`GITHUB_TOKEN`, `GITHUB_REPOSITORY`, and a PR number from `GITHUB_REF` or
258
+ `GITHUB_EVENT_PATH`); `GH_TOKEN` is ignored so a local `gh auth login` cannot
259
+ turn a laptop run into a comment. Delivery problems warn on stderr and leave
260
+ the exit code to the diff and policy result, and the token is never printed.
261
+ The bundled `flecto-ci` Action exposes this as the opt-in `pr-comment-post`
262
+ and `github-token` inputs. ([#67])
263
+ - Multi-document YAML support (`---`-separated), the usual shape of a Kubernetes
264
+ manifest. Previously such a file failed to parse. Each document is diffed
265
+ under its own key: `kind/name` for Kubernetes-shaped documents (namespaced
266
+ resources include the namespace), then a top-level `id` or `name`, falling
267
+ back to the document index when no stable identity is available — so a
268
+ document inserted at the top of a file no longer renumbers every other path.
269
+ Empty documents (a leading or trailing `---`, or a template that rendered
270
+ nothing) are dropped. Single-document files are unchanged: they still parse to
271
+ the document itself, with identical diff paths. ([#69])
272
+ - Stack-aware `flecto init`: the generated `.flectorc.json` now pre-selects
273
+ policy packs and file patterns from signals in the working directory —
274
+ `docker-compose.yml` / `compose.yaml` enables the `compose` pack and watches
275
+ the compose file, `package.json` enables `node-runtime`, and `config/` plus
276
+ `.env` files shape the `files` patterns. Terraform files are reported as
277
+ context only, since no `terraform` pack ships yet and `.tf` is not a parseable
278
+ format. `init` prints what it detected and why, and falls back to the previous
279
+ generic starter config when nothing is found. ([#72])
280
+ - `flecto compare <fileA> <fileB>`: run the differ and policy engine across two
281
+ different files, for environment skew ("works in staging, fails in prod")
282
+ rather than drift in one file over time. `fileA` is the baseline, so `+` is
283
+ present only in `fileB` and `-` only in `fileA`. The two files need not share
284
+ a format — `config/prod.yaml` against `config/prod.json` works, since every
285
+ supported format parses to a plain tree. Respects `--profile`, `--ignore`,
286
+ `--policies`, `--plugins`, `--array-id-key`, `--no-array-id`,
287
+ `--array-ignore-order`, and `--mask-secrets` exactly as `ci` does, and adds
288
+ `--fail-on` with the same triggers (defaulting to
289
+ `changed,added,removed,policy,error`, since environments that should match
290
+ ought to match on added and removed keys too). Output defaults to the
291
+ human-readable renderer; `--format json|ndjson|github-annotations` emits the
292
+ same envelopes and result shape as `ci`, plus a `baseline` field naming
293
+ `fileA`. Exit code is `0` when the files match under the active fail triggers,
294
+ `1` otherwise. ([#70])
295
+ - A reproducible large-repo benchmark harness (`npm run bench`) and the findings
296
+ it produced in [docs/performance.md](docs/performance.md). The harness
297
+ generates a synthetic repo at 50/250/1000 config files — including deeply
298
+ nested trees and files with 5,000-entry arrays — snapshots it, mutates it, and
299
+ then measures `flecto ci` end to end while attributing time across glob
300
+ discovery, snapshot load, parse, diff, and policy evaluation. It uses
301
+ `node:perf_hooks` only, adds no dependency, never runs during `npm test`, and
302
+ is excluded from the published package. Developer tooling: nothing in `src/`
303
+ or the CLI depends on it. ([#78])
304
+ - `flecto plan <planFiles...>`: diff Terraform plan JSON (`terraform show
305
+ -json`) with the same differ, envelope, and policy engine every other command
306
+ uses. Flecto never runs the `terraform` binary — it only reads the JSON you
307
+ hand it. Paths are keyed by the resource address
308
+ (`aws_security_group.web.ingress[0].cidr_blocks[0]`), and every resource also
309
+ gets a synthetic `#action` attribute so resource-level rules can match one
310
+ event instead of one per attribute: `create` reports as `added`, `delete` as
311
+ `removed`, `update` as `changed`, and — deliberately — a `replace`
312
+ (destroy-and-recreate, in either action ordering) also reports as `removed`
313
+ carrying the value `"replace"`, so `--fail-on removed` catches every replace
314
+ with no policy pack loaded, and the note names the attribute that forced it
315
+ (`(forced by: engine_version)`). Values Terraform cannot resolve until apply
316
+ (`after_unknown`) render as `(known after apply)`, never `null`, except on a
317
+ pure create, where an all-computed attribute is dropped rather than listed.
318
+ Values Terraform marks sensitive are replaced with `(sensitive value)`
319
+ unconditionally — before the policy engine, the envelope, or any formatter
320
+ sees them — independent of `--mask-secrets`; that flag adds Flecto's own
321
+ value-shaped detection on top, for credentials Terraform did not mark.
322
+ `--format human|json|ndjson|github-annotations|pr-comment`, `--ignore`,
323
+ `--policies` (default `terraform`), `--plugins`, and `--fail-on` (default
324
+ `error`, not `changed` — a plan is supposed to contain changes) all work as
325
+ they do elsewhere. Ships with a new `terraform` policy pack, loaded by
326
+ default: a resource replaced or a stateful resource destroyed, security-group
327
+ ingress opened to `0.0.0.0/0` / `::/0`, an IAM policy granting a wildcard
328
+ `Action`/`Resource`, an S3 public-access-block disabled or a public ACL, an
329
+ instance-size change, a capacity setting jumping 2x or more, any
330
+ Terraform-sensitive value changing, and a credential-shaped value Terraform
331
+ did not mark sensitive. See [docs/terraform.md](docs/terraform.md). ([#73])
332
+
333
+ ### Changed
334
+
335
+ - `flecto init` no longer claims to have initialized a config when one already
336
+ exists. It now checks every `.flectorc` candidate — not just
337
+ `.flectorc.json` — and warns that the existing file was left unchanged instead
338
+ of writing a second config that `loadRcConfig` would shadow. ([#72])
339
+ - Policy packs are now cached across a run instead of being re-resolved,
340
+ re-parsed, and re-validated on every file (`ci`) or every change event
341
+ (`watch`). The cache key is the working directory, the resolved pack path,
342
+ and that file's mtime, so a `policies/<id>.json` edited mid-`watch` is picked
343
+ up on the very next change event rather than served stale — watch mode's
344
+ fail-closed behavior on a bad pack edit is unchanged, and per-profile
345
+ `severityRemap` still applies after the cache, so one profile's remap can
346
+ never leak into another's findings. `matchClause()` also compiles each
347
+ rule's `match.path` and `afterMatches` regular expressions once at pack-load
348
+ time instead of once per change event. Measured with `npm run bench`: the
349
+ policy phase of the in-process pipeline at 1,000 files drops by roughly 60%
350
+ (median across two 15-run sessions: ~38 ms to ~15 ms); end-to-end `flecto ci`
351
+ wall time improves more modestly and closer to the harness's documented ±10%
352
+ run-to-run noise. See [docs/performance.md](docs/performance.md).
353
+ ([#92], [#93]) ([#108])
354
+
355
+ ### Fixed
356
+
357
+ - **SOPS protections no longer disengage on multi-document YAML.** Multi-document
358
+ files ([#69]) wrap each document in a synthetic identity-keyed object, so a
359
+ `sops` metadata block sits one level below the root — and `encryptionState` /
360
+ `normalizeEncrypted` looked only at the root. Every SOPS protection silently
361
+ switched off on exactly the file shape Kubernetes secrets ship in: the
362
+ plaintext of a value that had just been decrypted was printed verbatim (with
363
+ `--mask-secrets` on as well as off), no `sops` or `default` pack rule could
364
+ fire, and a recipient inserted at the front of a document's key list read as
365
+ "every recipient changed". A pull request that added an attacker's decryption
366
+ key *and* committed a secret in the clear passed `--fail-on policy,error`
367
+ while printing the secret into the CI log. Encryption state is now determined
368
+ per document, `sops` pack rules match a document-prefixed path, and recipient
369
+ groups inside a document are re-keyed by identity exactly as they are at the
370
+ root. Ciphertext itself never leaked — `redactCiphertext` always walked the
371
+ whole tree — and single-document behaviour is byte-for-byte unchanged.
372
+ ([#109])
373
+ - **`--mask-secrets` no longer masks every value in a document whose resource
374
+ name looks secret-shaped.** Secret-name matching ran against the whole diff
375
+ path, and a multi-document path begins with the document's identity, so any
376
+ resource whose kind or name contained `secret`, `token`, `password`,
377
+ `api_key`, `private_key`, or `credential` — every `kind: Secret`, and any
378
+ Deployment called something like `token-service` — had all of its values
379
+ replaced by `***`, numbers and booleans included. A reviewer could not see
380
+ that `replicas` went 2 → 9 or that `privileged` went false → true, and policy
381
+ messages interpolating those values degraded to nonsense. The parser now
382
+ records the keys it invented for a multi-document file and the renderers match
383
+ secret names against the path *below* that prefix: a resource name is user
384
+ data and never participates. Genuinely sensitive keys inside a document —
385
+ `data.password`, `stringData.token` — are masked exactly as before. Snapshots
386
+ of multi-document files record their document keys so `flecto report` and
387
+ `flecto history` mask correctly too; snapshots of ordinary files are
388
+ unchanged. ([#110])
389
+ - Policy finding messages no longer bypass secret masking. `evaluatePolicies`
390
+ runs on unmasked events, so a pack rule whose `messageTemplate` interpolates
391
+ `{before}` / `{after}` could print a credential that `--mask-secrets` had
392
+ redacted from `changes` — across the terminal, webhooks, CI JSON, GitHub
393
+ annotations, and the PR comment. Interpolated values are now masked with the
394
+ same path-aware logic as change events. ([#88])
395
+ - **Unknown `--fail-on` triggers are rejected instead of silently ignored.** A
396
+ typo such as `--fail-on polciy,eror` previously matched nothing, so the run
397
+ exited `0` with a real diff present and the CI gate was effectively absent.
398
+ Unknown triggers now fail with the list of valid ones. ([#97])
399
+ - YAML and TOML scalars are normalized to a JSON-safe tree before they reach
400
+ snapshots, the differ, or renderers — dates, BigInts, non-finite numbers, and
401
+ objects carrying a `toJSON()`. Previously these could diff or serialize
402
+ inconsistently depending on which parser produced them. Existing snapshots
403
+ are unaffected: `JSON.stringify` already wrote these as strings, so this makes
404
+ the in-memory tree match what was always on disk. ([#94])
405
+ - Top-level `include` patterns are merged with `files` instead of being dropped
406
+ whenever `files` was also present in `.flectorc`. ([#95])
407
+ - `--on-alert-failure exit` now terminates watch mode. It reported the failure
408
+ but left the watcher running, so a build depending on it to stop never did.
409
+ ([#96])
410
+ - `flecto watch` no longer misses changes when a file's valid JSON root is
411
+ `null`. The baseline was treated as absent rather than as the value `null`,
412
+ so the first real change after it went unreported. ([#98])
413
+ - `flecto watch --snapshot` no longer degrades quadratically with the number of
414
+ tracked files. Deciding whether a file already had snapshot history listed the
415
+ whole `.flecto-snapshots/` directory once per file — and compiled a regular
416
+ expression once per directory entry — so re-snapshotting a repo cost N
417
+ listings of O(N) entries. The directory is now listed once per run. Measured
418
+ on 1,000 tracked files, re-snapshotting went from 2,229 ms to 546 ms (~4x);
419
+ the first snapshot of a repo, which never took this path, is unchanged.
420
+ ([#78])
421
+ - `flecto ci --snapshot-ref <git-ref>` now resolves the baseline correctly when
422
+ run from a subdirectory of the repository. `git show <rev>:<path>` resolves
423
+ `<path>` from the repository root, so the previous cwd-relative path failed
424
+ outside the repo root — a common setup in monorepos. Paths are also
425
+ canonicalized before comparison, fixing baseline resolution under symlinked
426
+ directories such as macOS `/tmp` and `/var/folders`. ([#79])
427
+ - `--mask-secrets` now redacts nested secret values in terminal output
428
+ (`watch` and `watch --diff`), matching the masking already applied to
429
+ webhook/CI payloads. Previously a change on a benign-looking path such as
430
+ `database` printed its `password` / `api_key` children in the clear.
431
+ ([#24])
432
+ - A YAML file with a self-referential anchor (`a: &x\n b: *x`) no longer
433
+ fails to parse. js-yaml resolves such an alias to the same object it
434
+ anchors, producing a genuinely cyclic tree; scalar normalization walked it
435
+ and overflowed the call stack (`Maximum call stack size exceeded`) before
436
+ the file could load at all — a bare `Parse error`, not a crash. Cyclic
437
+ back-references now normalize to a fixed `"<circular>"` sentinel, so the
438
+ rest of the file parses, snapshots, and diffs normally, two files with the
439
+ same cycle shape compare equal, and merge keys (`<<: *base`), which resolve
440
+ to an ordinary acyclic tree, are unaffected. ([#103]) ([#107])
441
+
10
442
  ## [2.1.0] - 2026-07-24
11
443
 
12
444
  ### Added
@@ -71,7 +503,9 @@ The format is based on [Keep a Changelog], and this project adheres to
71
503
  - Misconfigured policy packs/plugins cause `watch` to exit non-zero instead of
72
504
  continuing with no policies.
73
505
 
74
- [Unreleased]: https://github.com/myselfsiddharth/Flecto/compare/v2.1.0...HEAD
506
+ [Unreleased]: https://github.com/myselfsiddharth/Flecto/compare/v3.0.1...HEAD
507
+ [3.0.1]: https://github.com/myselfsiddharth/Flecto/compare/v3.0.0...v3.0.1
508
+ [3.0.0]: https://github.com/myselfsiddharth/Flecto/compare/v2.1.0...v3.0.0
75
509
  [2.1.0]: https://github.com/myselfsiddharth/Flecto/compare/v2.0.0...v2.1.0
76
510
  [#6]: https://github.com/myselfsiddharth/Flecto/issues/6
77
511
  [#7]: https://github.com/myselfsiddharth/Flecto/issues/7
@@ -99,5 +533,34 @@ The format is based on [Keep a Changelog], and this project adheres to
99
533
  [#38]: https://github.com/myselfsiddharth/Flecto/issues/38
100
534
  [#39]: https://github.com/myselfsiddharth/Flecto/issues/39
101
535
  [#40]: https://github.com/myselfsiddharth/Flecto/pull/40
536
+ [#66]: https://github.com/myselfsiddharth/Flecto/issues/66
537
+ [#67]: https://github.com/myselfsiddharth/Flecto/issues/67
538
+ [#68]: https://github.com/myselfsiddharth/Flecto/issues/68
539
+ [#69]: https://github.com/myselfsiddharth/Flecto/issues/69
540
+ [#70]: https://github.com/myselfsiddharth/Flecto/issues/70
541
+ [#71]: https://github.com/myselfsiddharth/Flecto/issues/71
542
+ [#72]: https://github.com/myselfsiddharth/Flecto/issues/72
543
+ [#73]: https://github.com/myselfsiddharth/Flecto/issues/73
544
+ [#74]: https://github.com/myselfsiddharth/Flecto/issues/74
545
+ [#75]: https://github.com/myselfsiddharth/Flecto/issues/75
546
+ [#76]: https://github.com/myselfsiddharth/Flecto/issues/76
547
+ [#77]: https://github.com/myselfsiddharth/Flecto/issues/77
548
+ [#78]: https://github.com/myselfsiddharth/Flecto/issues/78
549
+ [#79]: https://github.com/myselfsiddharth/Flecto/issues/79
550
+ [#88]: https://github.com/myselfsiddharth/Flecto/issues/88
551
+ [#92]: https://github.com/myselfsiddharth/Flecto/issues/92
552
+ [#93]: https://github.com/myselfsiddharth/Flecto/issues/93
553
+ [#94]: https://github.com/myselfsiddharth/Flecto/issues/94
554
+ [#95]: https://github.com/myselfsiddharth/Flecto/issues/95
555
+ [#96]: https://github.com/myselfsiddharth/Flecto/issues/96
556
+ [#97]: https://github.com/myselfsiddharth/Flecto/issues/97
557
+ [#98]: https://github.com/myselfsiddharth/Flecto/issues/98
558
+ [#103]: https://github.com/myselfsiddharth/Flecto/issues/103
559
+ [#104]: https://github.com/myselfsiddharth/Flecto/issues/104
560
+ [#107]: https://github.com/myselfsiddharth/Flecto/pull/107
561
+ [#108]: https://github.com/myselfsiddharth/Flecto/pull/108
562
+ [#109]: https://github.com/myselfsiddharth/Flecto/issues/109
563
+ [#110]: https://github.com/myselfsiddharth/Flecto/issues/110
102
564
  [Keep a Changelog]: https://keepachangelog.com/en/1.1.0/
103
565
  [Semantic Versioning]: https://semver.org/spec/v2.0.0.html
566
+ [GHSA-wq8m-fc3q-8m5x]: https://github.com/myselfsiddharth/Flecto/security/advisories/GHSA-wq8m-fc3q-8m5x