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 +464 -1
- package/README.md +348 -304
- package/index.js +586 -61
- package/package.json +3 -2
- package/schemas/flecto-policy-pack-2.0.json +5 -0
- package/src/alerter.js +20 -3
- package/src/config.js +173 -22
- package/src/differ.js +59 -2
- package/src/documents.js +106 -0
- package/src/encrypted.js +573 -0
- package/src/notifiers.js +430 -0
- package/src/packs/default.json +22 -0
- package/src/packs/kubernetes.json +112 -0
- package/src/packs/sops.json +61 -0
- package/src/packs/strict-prod.json +10 -0
- package/src/packs/terraform.json +120 -0
- package/src/parser.js +189 -20
- package/src/policy.js +498 -11
- package/src/pr-comment.js +480 -0
- package/src/renderer.js +70 -16
- package/src/report.js +653 -0
- package/src/secrets.js +316 -0
- package/src/terraform.js +500 -0
- package/src/watcher.js +9 -7
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/
|
|
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
|