border-customs 0.8.0 → 0.8.2
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/README.md +224 -18
- package/dist/assets/exfil-src/rules.ts +1 -1
- package/dist/assets/exfil-src/rulesPath.ts +28 -0
- package/dist/assets/exfil-src/scan.ts +2 -0
- package/dist/assets/exfil-src/severity.ts +7 -3
- package/dist/assets/gitleaks-defaults-v8.30.1.toml +24 -0
- package/dist/assets/package-lock.json +69 -12
- package/dist/assets/residue-src/crates.ts +60 -31
- package/dist/assets/residue-src/npm.ts +35 -8
- package/dist/assets/residue-src/pypi.ts +31 -11
- package/dist/assets/residue-src/rubygems.ts +43 -20
- package/dist/index.js +2617 -4041
- package/package.json +9 -6
- package/plugin/border.ts +33 -9
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ cd /path/to/your/repo
|
|
|
10
10
|
border check
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
Requires Node >= 22 and the `gitleaks` binary on `PATH` (border vendors the rule config for
|
|
13
|
+
Requires Node >= 22.22.2 (the transitive `ini@7` dependency sets that floor) and the `gitleaks` binary on `PATH` (border vendors the rule config for
|
|
14
14
|
gitleaks 8.30.1; the binary itself is yours to install, same as `git`). secretlint runs
|
|
15
15
|
in-process as a bundled dependency. Each registry leg additionally shells out to its platform's
|
|
16
16
|
own publisher, which must be on `PATH` whenever that target is configured; a missing publisher
|
|
@@ -18,14 +18,29 @@ binary is exit 2, never a skipped leg:
|
|
|
18
18
|
|
|
19
19
|
| Registry target | Publisher border runs | Version the same-bytes proofs were measured on |
|
|
20
20
|
| --- | --- | --- |
|
|
21
|
-
| `npm` | `npm` | (
|
|
21
|
+
| `npm` | `npm` | npm >= 11 for packages with pack-phase hooks (see below) |
|
|
22
22
|
| `pypi` | `python3 -m build` / `python3 -m twine` | (unchanged since 0.1.0) |
|
|
23
23
|
| `crates` | `cargo` | cargo 1.93.1 (rustup) |
|
|
24
24
|
| `rubygems` | `gem` | RubyGems 3.6.7 on ruby 3.3.8 |
|
|
25
25
|
|
|
26
|
+
**npm pack safety probe.** `npm pack --ignore-scripts` is not trustworthy on npm 10: measured
|
|
27
|
+
on npm 10.9.7, the target package's `prepare` script still executes (npm 11.19.1 is clean).
|
|
28
|
+
Before packing, border runs `npm --version`. On npm < 11 it refuses, with a named exit 2, any
|
|
29
|
+
package that declares a `prepack`, `prepare`, or `postpack` script — border never executes the
|
|
30
|
+
code it audits. A package with none of those hooks has nothing for npm to run and stays
|
|
31
|
+
scannable on npm 10. Installing npm >= 11 removes the refusal.
|
|
32
|
+
|
|
26
33
|
Other toolchain versions of `cargo` and `gem` are untested by those byte-determinism proofs.
|
|
27
34
|
Optional engines are listed in [Configuration](#configuration).
|
|
28
35
|
|
|
36
|
+
**Pinned consumption.** Install an exact version (`npm install -g border-customs@<version>`;
|
|
37
|
+
`npx border-customs@<version> check` for a one-off) instead of running gate code out of a
|
|
38
|
+
source checkout. Every check and push record carries `engineBuild` — the identity
|
|
39
|
+
(`<version>+<commit>[.dirty]`) of the border build that produced the verdict, the same string
|
|
40
|
+
`border --version` prints. A `.dirty` suffix means the verdict came from uncommitted code, and
|
|
41
|
+
records from one repo carrying different `engineBuild` values mean the audit trail is mixed:
|
|
42
|
+
both are findings an auditor should chase, which is exactly why the identity is on the record face.
|
|
43
|
+
|
|
29
44
|
## The problem it solves
|
|
30
45
|
|
|
31
46
|
The usual secret workflow is leak-then-repent: `git push`, notice the AWS key in the diff
|
|
@@ -118,7 +133,18 @@ it, and the two registry-facing legs (pre-flight and publish) both have to agree
|
|
|
118
133
|
against the history of every ref *and* against the object set a remote does not already
|
|
119
134
|
have (the transmit set). Oversized-file, checked-in-binary, and notebook-output rules
|
|
120
135
|
round it out.
|
|
121
|
-
7. **
|
|
136
|
+
7. **Landing-grade tip lens.** The exact machinery `border push` uses to verify a landing
|
|
137
|
+
(`scanTipTree`: the tip tree materialized from git objects into a throwaway directory,
|
|
138
|
+
scanned by the native exfil family plus both twins filtered to `exfil-*` ids) also runs
|
|
139
|
+
here, against local HEAD — the tree a push would transmit. This closes the
|
|
140
|
+
disk-versus-tip gap: content that exists only in HEAD (introduced by a merge commit's
|
|
141
|
+
resolution and since deleted from disk, say) is invisible to the history and working-tree
|
|
142
|
+
legs above but cannot hide from the gate. Tip findings merge before the allow-list, so a
|
|
143
|
+
config `allow` pin exempts them by rule+file+digest exactly like every other finding, and
|
|
144
|
+
the post-push landing check honors the *same* pins — a green packet means green landing, in
|
|
145
|
+
both directions. The tip leg's git plumbing is fail-closed: a git failure aborts the run
|
|
146
|
+
(exit 2), never a silent clean.
|
|
147
|
+
8. **Registry pre-flight** (npm, PyPI, crates.io, and RubyGems targets). Three outcomes per
|
|
122
148
|
target: version already published ⇒ CRITICAL `version-exists` ("bump version required");
|
|
123
149
|
name owned by someone outside your `rules.authors` allow-list ⇒ CRITICAL
|
|
124
150
|
`name-foreign-owner`; ambiguous ownership signals ⇒ also CRITICAL, because guessing wrong
|
|
@@ -129,7 +155,7 @@ it, and the two registry-facing legs (pre-flight and publish) both have to agree
|
|
|
129
155
|
one of that API's exact *absent* messages (its 404s are text, not JSON, and an
|
|
130
156
|
unrecognized one is never guessed), a timeout, or unparseable JSON all fail closed as
|
|
131
157
|
exit 2. An unreachable registry blocks the push instead of letting it run blind.
|
|
132
|
-
|
|
158
|
+
9. **Artifact stage.** If registry targets are configured, packages are built **once** into
|
|
133
159
|
`.border/dist/` (`npm pack --ignore-scripts`, `python -m build --no-isolation`,
|
|
134
160
|
`cargo package --allow-dirty --no-verify`, `gem build <name>.gemspec -o .border/dist/`),
|
|
135
161
|
that exact byte-stream is scanned (gitleaks + secretlint over the extracted contents),
|
|
@@ -145,14 +171,16 @@ it, and the two registry-facing legs (pre-flight and publish) both have to agree
|
|
|
145
171
|
re-runs the check's exact `cargo package` into a throwaway directory and refuses to
|
|
146
172
|
publish if the fresh digest differs from the certified `.crate`. `gem push` uploads the
|
|
147
173
|
recorded `.gem` byte-for-byte, so it needs no such re-assertion.
|
|
148
|
-
|
|
174
|
+
10. **Report.** Findings carry `valueDigest` (sha256 of the matched value) and a masked
|
|
149
175
|
snippet: fully blocked for short values, else `first4…last4`. Raw secret bytes never
|
|
150
176
|
leave process memory. A per-run `TextSanitizer` holds the digest-to-value registry and
|
|
151
177
|
replaces every flagged literal in *all* rendered text, including agent-written strings in
|
|
152
178
|
the LLM layer, so a secret cannot reappear inside a message about itself. `report.json`
|
|
153
179
|
(canonical, byte-stable for machines) and `report.md` land under `.border/runs/`.
|
|
154
|
-
|
|
155
|
-
and artifact digests. Degraded and NO-OP runs can never write a PASS.
|
|
180
|
+
11. **Ledger record.** A PASS is appended to `.border/ledger.jsonl` together with the key
|
|
181
|
+
and artifact digests. Degraded and NO-OP runs can never write a PASS. The key folds in a
|
|
182
|
+
*lens identity*, so a PASS recorded under an older, weaker set of legs never certifies a
|
|
183
|
+
push under the current lens — upgrading the gate forces one fresh full check.
|
|
156
184
|
|
|
157
185
|
```bash
|
|
158
186
|
$ border check # full scan, ~seconds on a mid-size repo
|
|
@@ -194,7 +222,7 @@ an installer *does*, per tier — the split behind opencode-ai's in-tree `postin
|
|
|
194
222
|
The pairing classes behind these rows (multi-channel PASS **C5** subset/ordering discipline;
|
|
195
223
|
**C9** interpolated-and-indirected root spellings; **C10** comment/quote-boundary shapes that
|
|
196
224
|
once silent-passed) are frozen with their evidence in
|
|
197
|
-
[src/artifacts/RESIDUE-CONTRACT.md](src/artifacts/RESIDUE-CONTRACT.md).
|
|
225
|
+
[src/artifacts/RESIDUE-CONTRACT.md](https://github.com/TachikomaGundam/Border-Customs/blob/main/src/artifacts/RESIDUE-CONTRACT.md).
|
|
198
226
|
|
|
199
227
|
**Fail-closed:** a residue scan whose rule table or classifier sources cannot be read is exit 2,
|
|
200
228
|
never a silent pass — and the classifier source bytes are part of the ledger `rulesHash`, so
|
|
@@ -209,10 +237,30 @@ clean residue scan is an absence of matched signatures, never a certificate of b
|
|
|
209
237
|
The signature table is deliberately closed, and the list of shapes it **cannot see** —
|
|
210
238
|
content-writes, handle-variants, dead spellings, interpolation-then-slash, span-quirk
|
|
211
239
|
over-blocks, IO-instance writers, user-shadowed bare verbs — lives in
|
|
212
|
-
[RESIDUE-CONTRACT.md §8](src/artifacts/RESIDUE-CONTRACT.md); this README points, it does not
|
|
240
|
+
[RESIDUE-CONTRACT.md §8](https://github.com/TachikomaGundam/Border-Customs/blob/main/src/artifacts/RESIDUE-CONTRACT.md); this README points, it does not
|
|
213
241
|
restate. The empirical roundtrip (install → snapshot → uninstall → rc-delta-zero) is the 0.4.0
|
|
214
|
-
valve. **border
|
|
215
|
-
|
|
242
|
+
valve. **border does not execute the code it audits.** Every scan leg (gitleaks, secretlint,
|
|
243
|
+
the native rules, the exfil lens) is pure byte inspection; the cross-channel marker fixture
|
|
244
|
+
(`test/build.execMarkers.test.ts`) pins that a consentless `border check` spawns ZERO build
|
|
245
|
+
toolchains. Registry artifact builds (npm pack, `python -m build`, `gem build`, `cargo
|
|
246
|
+
package`) DO execute repo-supplied build code, so they run only on operator consent:
|
|
247
|
+
`--allow-builds`, or `allowBuilds: true` in the machine store / an explicit `--config` file
|
|
248
|
+
outside the repo (the environment is deliberately NOT a consent source — a hostile `.envrc`
|
|
249
|
+
owns it). Both sources live OUTSIDE the audited tree, and a repo's own border.yaml can declare
|
|
250
|
+
targets but can never enable building them. Without
|
|
251
|
+
consent the run is scan-only: no artifact digests are recorded (visible as MEDIUM
|
|
252
|
+
`build-stage-withheld` findings), and a scan-only PASS can never back a registry publish.
|
|
253
|
+
Consented builds can additionally run inside a **pinned container** (`targets.<ch>.sandboxImage`,
|
|
254
|
+
see Configuration): the container is launched with the network disabled — the `--network none`
|
|
255
|
+
argv the daemon applies is the enforcement anchor, and an in-container probe re-checks DNS and
|
|
256
|
+
TCP from inside as a tripwire — the repo mounted read-only and built from a tmpfs copy, host
|
|
257
|
+
credentials never entering the container, and every ledger record bound to the exact image
|
|
258
|
+
digest that certified its bytes. `sandboxImage` itself is a security-tier key: honored only
|
|
259
|
+
from the machine store or a `--config` file outside the repo whose ownership and modes pass
|
|
260
|
+
the fd-chain qualification, and any competing declaration (repo config, lax-mode file, hijacked
|
|
261
|
+
`XDG_CONFIG_HOME`) is a hard refusal, never a silent fallback. Border is
|
|
262
|
+
containment-by-consent plus optional container containment — it is not a malware sandbox and
|
|
263
|
+
not a VM: a kernel-level escape from the container remains in the threat model.
|
|
216
264
|
|
|
217
265
|
#### Proof valve: `residue.requireProof` (0.4.0)
|
|
218
266
|
|
|
@@ -251,9 +299,10 @@ lives inside. The exfil lens adds five rules over the surfaces about to become p
|
|
|
251
299
|
| `exfil-home-path` | `/home/<user>` and `C:\Users\<user>` shapes |
|
|
252
300
|
| `exfil-cred-location` | `~/.pypirc`, `SSHPASS`, `*.env` credential-location shapes |
|
|
253
301
|
| `exfil-host-profile` | multi-signal host fingerprints (os+version / no-docker / no-passwordless-sudo co-occurrences in one paragraph) |
|
|
302
|
+
| `exfil-machine-binding` | vendor inference-model ids pinned on portable surfaces next to model/default/provider context (L-MACHINE-LOCAL; born 2026-10-02) |
|
|
254
303
|
|
|
255
304
|
The severity a reader **observes** is the (rule × facet × channel) matrix, and its single
|
|
256
|
-
home is [src/exfil/severity.ts](src/exfil/severity.ts) — the check pipeline, the
|
|
305
|
+
home is [src/exfil/severity.ts](https://github.com/TachikomaGundam/Border-Customs/blob/main/src/exfil/severity.ts) — the check pipeline, the
|
|
257
306
|
`border exfil` CLI and this table all render that table, never re-decide it; the
|
|
258
307
|
`EXFIL-DOC` guard test fails the build if a cell is restated wrong:
|
|
259
308
|
|
|
@@ -264,11 +313,13 @@ home is [src/exfil/severity.ts](src/exfil/severity.ts) — the check pipeline, t
|
|
|
264
313
|
| `exfil-home-path` | tree | native | MEDIUM |
|
|
265
314
|
| `exfil-cred-location` | tree | native | MEDIUM |
|
|
266
315
|
| `exfil-host-profile` | tree | native | MEDIUM |
|
|
316
|
+
| `exfil-machine-binding` | tree | native | MEDIUM |
|
|
267
317
|
| `exfil-rfc1918` | message | native | HIGH |
|
|
268
318
|
| `exfil-ssh-target` | message | native | HIGH |
|
|
269
319
|
| `exfil-home-path` | message | native | MEDIUM |
|
|
270
320
|
| `exfil-cred-location` | message | native | MEDIUM |
|
|
271
321
|
| `exfil-host-profile` | message | native | MEDIUM |
|
|
322
|
+
| `exfil-machine-binding` | message | native | MEDIUM |
|
|
272
323
|
|
|
273
324
|
Why an observed `CRITICAL` stands where the intent table says HIGH: the twin channels have
|
|
274
325
|
no HIGH rung — every ingested gitleaks finding carries CRITICAL, and secretlint's `error`
|
|
@@ -411,7 +462,10 @@ never from cache:
|
|
|
411
462
|
and exits 1; a rerun of `border push --yes` picks up only the still-PENDING targets.
|
|
412
463
|
- After every git leg that moved, border **verifies the landing**: it re-observes the remote
|
|
413
464
|
tip (`git ls-remote`, then a fetch of exactly that tip when it is not local) and scans the
|
|
414
|
-
landed tree with the same exfil machinery as `border exfil
|
|
465
|
+
landed tree with the same exfil machinery as `border exfil` — the same lens `border check`
|
|
466
|
+
now runs pre-push, and the same config `allow` pins apply: blocking counts are computed
|
|
467
|
+
after the allow-list, and any exemption is echoed loudly (rule, entry index, count), never
|
|
468
|
+
silent. Three honest outcomes: clean
|
|
415
469
|
appends a `t:"landing"` ledger record and preserves the push exit; a blocking hit exits 1
|
|
416
470
|
with `ALREADY PUBLIC; border detects, never erases` — the bytes are out, and border's job
|
|
417
471
|
there is detection, recording the fact, never rewriting history; an unobservable tip exits
|
|
@@ -589,11 +643,60 @@ degraded run.
|
|
|
589
643
|
|
|
590
644
|
## Configuration
|
|
591
645
|
|
|
592
|
-
|
|
646
|
+
Border is a property of the **checking machine**, not of the checked repo. Config resolves
|
|
647
|
+
in this order, and the machine-side rungs exist so gate config (including waivers) never has
|
|
648
|
+
to be committed into a product repo:
|
|
649
|
+
|
|
650
|
+
1. `--config <path>` (explicit);
|
|
651
|
+
2. `$BORDER_CONFIG` — fail-closed: set-but-empty or unreadable is exit 2, never a fall-through;
|
|
652
|
+
3. the **machine store**: `$BORDER_CONFIG_DIR` (else `$XDG_CONFIG_HOME/border/machines`, else
|
|
653
|
+
`~/.config/border/machines`) holding `<sha256-16>.yaml` files keyed by the repo's canonical
|
|
654
|
+
remote URL (`host/path`, credentials/`.git`/scp-vs-https spellings collapsed);
|
|
655
|
+
4. `./border.yaml` (cwd, then git toplevel) — kept for repos that *choose* to carry their config;
|
|
656
|
+
5. git-remote inference (targets only, no waivers) when nothing above exists.
|
|
657
|
+
|
|
658
|
+
Unknown keys are
|
|
593
659
|
rejected at load (a typo in a gate config is a gate you did not ask for). `${VAR}`
|
|
594
660
|
expansion applies only to remote URLs and registry/repository/host values, and an unset variable
|
|
595
661
|
is a hard error, never an empty string.
|
|
596
662
|
|
|
663
|
+
#### Sandboxed builds (`targets.<ch>.sandboxImage`)
|
|
664
|
+
|
|
665
|
+
Configure pins with the helper instead of hand-editing the machine store:
|
|
666
|
+
`border sandbox init --lane npm --image node@sha256:<64hex>` (writes only the
|
|
667
|
+
0600 machine-store file under the §3-qualified root, refuses tmp/unowned dirs and
|
|
668
|
+
tag refs); `border sandbox status` shows what the loader will actually honor;
|
|
669
|
+
`unpin` removes keys. The helper never writes into the audited repo.
|
|
670
|
+
|
|
671
|
+
When a lane declares `sandboxImage`, that lane's consented build runs inside `docker run`
|
|
672
|
+
instead of on the host: `--network none` (the daemon-applied argv is the enforcement anchor;
|
|
673
|
+
a DNS+TCP probe re-checks from *inside* as a tripwire that fails the build closed), read-only repo mount, build on a tmpfs snapshot, `--cap-drop ALL`,
|
|
674
|
+
`no-new-privileges`, and a scrubbed env (image defaults + `HOME=/tmp` only). The image MUST be
|
|
675
|
+
digest-pinned:
|
|
676
|
+
|
|
677
|
+
```yaml
|
|
678
|
+
targets:
|
|
679
|
+
npm:
|
|
680
|
+
sandboxImage: node@sha256:<64 hex> # tags are rejected at load — a mutable tag
|
|
681
|
+
# would let check and push certify different bytes
|
|
682
|
+
```
|
|
683
|
+
|
|
684
|
+
`sandboxImage` is a **security-tier key**: it is honored only from the machine store or an
|
|
685
|
+
explicit `--config` file outside the repo, and only after fd-chain qualification (every path
|
|
686
|
+
component verified by open file descriptor: root-owned or euid-owned above your trust anchor,
|
|
687
|
+
euid-owned `0700`-below files `0600` below it — symlink or mode violations fail closed).
|
|
688
|
+
Any other declaration of the same key — the repo's own border.yaml, a lax-mode (group/world
|
|
689
|
+
readable or writable) store file, an `XDG_CONFIG_HOME` override pointing somewhere
|
|
690
|
+
unqualifiable — is a named hard refusal (`sandbox-config-shadowed` /
|
|
691
|
+
`sandbox-config-unqualified`), never a silent ignore and never a host fallback.
|
|
692
|
+
|
|
693
|
+
Freshness and push carry the same binding: a PASS certified inside image X is re-proved only
|
|
694
|
+
inside X (config drift forces a full re-check), and docker being *absent* while an image is
|
|
695
|
+
configured is exit 2 with the remedy named — never a silent host fallback. No `sandboxImage`
|
|
696
|
+
means today's host build, unchanged. The crates lane keeps its registry cache in
|
|
697
|
+
`.border/cache/` (the one writable bind); the pypi image must ship the build backend's
|
|
698
|
+
dependencies (there is no network inside).
|
|
699
|
+
|
|
597
700
|
Minimal, a git-only repo:
|
|
598
701
|
|
|
599
702
|
```yaml
|
|
@@ -644,8 +747,15 @@ rules:
|
|
|
644
747
|
ips: ["192.0.2.1"]
|
|
645
748
|
pathPatterns: ["/Users/*", "*.pfx"]
|
|
646
749
|
maxFileKB: 500 # oversized-file threshold (default)
|
|
750
|
+
aliases: # optional: rule-rename migration for allow pins
|
|
751
|
+
old-rule-name: current-rule-name # {renamed-from: current} — waivers survive renames
|
|
647
752
|
allow: # enumerated suppressions, never blanket
|
|
648
753
|
- { rule: "junk-artifact", match: "*", file: "test/fixtures/**" }
|
|
754
|
+
- rule: "gitleaks:*" # fixture-set waiver: values pins the exact
|
|
755
|
+
file: "tests/fixtures/**" # value digests; ANY value outside stays red
|
|
756
|
+
values: ["<64hex>", "<64hex>"]
|
|
757
|
+
until: "2027-01-01" # optional UTC sunset; after it the entry is
|
|
758
|
+
# inert and echoed as allow-entry-expired
|
|
649
759
|
engines:
|
|
650
760
|
require: [gitleaks, secretlint] # trufflehog: true adds the third-party engine
|
|
651
761
|
residue:
|
|
@@ -656,6 +766,15 @@ release:
|
|
|
656
766
|
|
|
657
767
|
Notes that change behavior:
|
|
658
768
|
|
|
769
|
+
- **Archive member addressing in `allow.file`:** findings inside archives are attributed
|
|
770
|
+
`archive!<inner>`. A plain `file` glob matches only the truncated OUTER path (`archive`),
|
|
771
|
+
so member-level pins need the escape form `archive\\!inner` (the `\!` switches the entry to
|
|
772
|
+
full-attribution matching). Members are addressable; forgetting the escape silently matches
|
|
773
|
+
the whole archive instead.
|
|
774
|
+
- **Waiver floors:** a `values` entry suppresses only the listed value digests — a new value
|
|
775
|
+
under the same path/rule stays red; an `until` entry is inert after its UTC date and is
|
|
776
|
+
echoed as an INFO `allow-entry-expired` finding instead of quietly keeping work.
|
|
777
|
+
|
|
659
778
|
- `targets.git.remotes: []` (explicit, empty) is a *deliberate* repo-local scope: all
|
|
660
779
|
history/tree/identity/artifact legs still run, only the exposure set is empty. A
|
|
661
780
|
completely missing config with real remotes falls back to config-from-`git remote` with
|
|
@@ -700,7 +819,10 @@ the hook must first pass. Drop `--force` to get sub-second skip-ledger replay in
|
|
|
700
819
|
re-scan on every `git push`.
|
|
701
820
|
|
|
702
821
|
**CI.** `border check --json` emits the stable report object; gate on the exit code, parse
|
|
703
|
-
`verdict`/`counts` for dashboards.
|
|
822
|
+
`verdict`/`counts` for dashboards. Each finding is
|
|
823
|
+
`{ rule, severity, target, path?, line?, commit?, engine, message, valueDigest, snippet }` —
|
|
824
|
+
the file location is **`path`** (absent when the finding is commit-only, e.g. an identity
|
|
825
|
+
finding); there is no `file` field. In CI you typically want `--force` on a cache-less
|
|
704
826
|
runner, plus your `gitleaks` binary provisioned in the image (a degraded run exits 2, which
|
|
705
827
|
most CI maps to red; that is the point).
|
|
706
828
|
|
|
@@ -717,7 +839,7 @@ plugin entry ships as `exports["./server"]`):
|
|
|
717
839
|
```jsonc
|
|
718
840
|
{
|
|
719
841
|
// pin the exact version — an @latest channel hits the registry on every cold start
|
|
720
|
-
"plugin": ["border-customs@0.8.
|
|
842
|
+
"plugin": ["border-customs@0.8.2"]
|
|
721
843
|
}
|
|
722
844
|
```
|
|
723
845
|
|
|
@@ -813,6 +935,10 @@ which is now a thin consumer of `inspect --json` with the rc contract unchanged
|
|
|
813
935
|
- **Fail-closed doctrine.** "Silence never means absent" is applied mechanically: empty
|
|
814
936
|
registry stdout, unparsed versions, non-translatable exit codes, unresolvable ownership,
|
|
815
937
|
corrupt-but-present rules inputs, all become exit 2 or a CRITICAL, never a pass.
|
|
938
|
+
- **Platform behaviour is probed, not assumed.** The npm stage reads `npm --version` before
|
|
939
|
+
packing. On npm < 11 a declared pack-phase hook is a named exit 2, because that npm line
|
|
940
|
+
executes `prepare` despite `--ignore-scripts` (reproduced on npm 10.9.7, 2026-10-19); a
|
|
941
|
+
missing or unparseable version is also exit 2.
|
|
816
942
|
- **No telemetry, no callbacks.** `border check` touches the network only for the
|
|
817
943
|
registry pre-flight, against the URLs the channels define: `registry.npmjs.org`,
|
|
818
944
|
`pypi.org`, and `rubygems.org` are overridable (private indexes; `targets.rubygems.host`),
|
|
@@ -824,9 +950,89 @@ which is now a thin consumer of `inspect --json` with the rc contract unchanged
|
|
|
824
950
|
it, and which every scan leg refuses to treat as a finding source or an allow-list
|
|
825
951
|
target. A repo that *tracks* `.border/**` is itself a CRITICAL finding. A single-writer
|
|
826
952
|
lock makes concurrent runs exit 2 instead of racing.
|
|
953
|
+
- **Reports go through a private channel.** See [SECURITY.md](SECURITY.md) for the
|
|
954
|
+
reporting route and scope. Do not put exploit details or live secrets in a public issue.
|
|
955
|
+
|
|
956
|
+
## Development
|
|
957
|
+
|
|
958
|
+
Contributor setup, the exact external-engine list, subset test commands, and the version
|
|
959
|
+
rotation procedure live in [CONTRIBUTING.md](CONTRIBUTING.md). In short:
|
|
960
|
+
|
|
961
|
+
```bash
|
|
962
|
+
npm ci
|
|
963
|
+
npm run typecheck
|
|
964
|
+
npm test
|
|
965
|
+
```
|
|
966
|
+
|
|
967
|
+
CI runs the full suite on Node 22 (npm 10) and Node 24 (npm 11) for every push to `main`
|
|
968
|
+
and every pull request, plus version single-source, typecheck, build, and audit gates.
|
|
827
969
|
|
|
828
970
|
## Changelog
|
|
829
971
|
|
|
972
|
+
### 0.8.2 (2026-11-01)
|
|
973
|
+
- Production dependency graph now measures ZERO advisories (`npm audit --omit=dev`): the
|
|
974
|
+
pattern rule is vendored (`src/engines/borderPatternRule.ts`, MIT attribution; drops the
|
|
975
|
+
unpatchable braces/micromatch + fast-uri chain) and `tool` — an identity function — is
|
|
976
|
+
vendored in `plugin/border.ts`, moving `@opencode-ai/plugin` (and its effect-beta → toml
|
|
977
|
+
chain) to devDependencies; live-host probe tests keep the activation proof. The CI audit
|
|
978
|
+
gate tightens from critical to HIGH so this cannot rot.
|
|
979
|
+
- `border sandbox init|pin|status|unpin` — the opt-in pin helper from the Q1 ruling
|
|
980
|
+
(machine store only, 0600 atomic writes, §3 self-verification with rollback of its own
|
|
981
|
+
writes; deliberately absent from the plugin's agent-reachable command set).
|
|
982
|
+
- Security tier for execution-deciding keys (`src/tier.ts`, sibyl §3⁗/§3⁖/§3‡):
|
|
983
|
+
`sandboxImage`/`allowBuilds` honor only from fd-chain-qualified machine-store or out-of-repo
|
|
984
|
+
`--config` sources; repo declarations, lax-mode files, and hijacked `XDG_CONFIG_HOME` are
|
|
985
|
+
named refusals (`sandbox-config-shadowed`/`sandbox-config-unqualified`); the
|
|
986
|
+
`BORDER_ALLOW_BUILDS` environment channel is REMOVED (never released, so no migration);
|
|
987
|
+
passwd-derived canonical store root — `$HOME` spoofing cannot move it.
|
|
988
|
+
- Sandboxed artifact builds: optional `targets.<ch>.sandboxImage` (digest-pinned, tags fail
|
|
989
|
+
closed at load) runs consented npm/pypi/crates/rubygems builds — and freshness re-packs —
|
|
990
|
+
inside `docker run` with `--network none` proved by an in-container DNS+TCP probe, a
|
|
991
|
+
read-only repo mount, tmpfs build, `--cap-drop ALL`, scrubbed env, and the ledger record
|
|
992
|
+
bound to the exact image digest (a rotated image invalidates cached PASS). Missing docker
|
|
993
|
+
with an image configured is a named exit 2, never a host fallback. New `test/sandbox.test.ts`
|
|
994
|
+
pins the docker argv contract token by token.
|
|
995
|
+
- Hardened the npm pack-phase guard (sibyl must_fix): ALL package.json manifests in the tree
|
|
996
|
+
are scanned, not just the root (a workspaces child's `prepare` is now refused on npm < 11),
|
|
997
|
+
and unreadable manifests or a walk-cap hit count as a hook — unknown is never assumed safe.
|
|
998
|
+
- Machine-side config (HR decoupling handoff 2026-10-23): resolution chain is now
|
|
999
|
+
`--config > $BORDER_CONFIG (fail-closed) > machine store (XDG/border/machines, keyed by
|
|
1000
|
+
canonical remote URL) > ./border.yaml > git-remote inference`, so gate config and waiver
|
|
1001
|
+
lists live on the checking machine and never need to be committed into a product repo.
|
|
1002
|
+
Allow entries gained `values` (fixture-set waivers that never cover a new value), `until`
|
|
1003
|
+
(UTC sunset, expired entries echoed never silently honored), `\!` archive-member escaping,
|
|
1004
|
+
and `rules.aliases` (rule renames no longer strand waivers). Border state was already
|
|
1005
|
+
porcelain-invisible (`.border/.gitignore` self-ignore; verified 2026-10-23).
|
|
1006
|
+
- Provenance: every check and push record carries `engineBuild`
|
|
1007
|
+
(`<version>+<commit>[.dirty]`, the string `border --version` prints), so a verdict names
|
|
1008
|
+
the exact border build that produced it. Motivated by a machine where every gate ran off
|
|
1009
|
+
an unpinned worktree bundle and no record said so; worktree use is now visible on the
|
|
1010
|
+
record face (see "Pinned consumption"). Bundles are stamped by `tools/build.mjs`.
|
|
1011
|
+
- Security (breaking): artifact build stages now require operator consent. A hostile clone can
|
|
1012
|
+
ship its own border.yaml declaring pypi/rubygems targets, and `border check` built them —
|
|
1013
|
+
PEP 517 backends and gemspecs ARE code (`python3 -m build --no-isolation` and `gem build`
|
|
1014
|
+
markers reproduced 2026-10-23). Default runs are now scan-only (zero build spawns, pinned by
|
|
1015
|
+
`test/build.execMarkers.test.ts`); consent comes only from outside the audited tree:
|
|
1016
|
+
`--allow-builds` or `BORDER_ALLOW_BUILDS=1`. A scan-only PASS records no artifact digests and
|
|
1017
|
+
therefore can never back a registry publish; `border scan` of third-party artifacts requires
|
|
1018
|
+
the same consent (named exit 2 without it).
|
|
1019
|
+
- Security: refuse npm < 11 packages that declare `prepack`/`prepare`/`postpack` before
|
|
1020
|
+
spawning `npm pack`. npm 10 executes `prepare` even with `--ignore-scripts` (reproduced on
|
|
1021
|
+
10.9.7; npm 11.19.1 is clean), so the old defense could execute code from the audited repo.
|
|
1022
|
+
The probe also runs before freshness re-packs. New regression tests pin the refusal and the
|
|
1023
|
+
never-fires hook marker.
|
|
1024
|
+
- Fix: the Windows drive-letter path pattern no longer fires on JS escapes in prose
|
|
1025
|
+
(`raw-repoPath:\n`); the UNC pattern no longer spans newlines.
|
|
1026
|
+
- Docs: the `--json` findings schema is stated explicitly (`path`, no `file` alias);
|
|
1027
|
+
npm-target prerequisites now name the npm 11 boundary.
|
|
1028
|
+
|
|
1029
|
+
### 0.8.1 (2026-10-06)
|
|
1030
|
+
- Maintenance release: certificate-chain rebuild off the scrubbed lineage (INCIDENT-20261006
|
|
1031
|
+
identity-exposure cleanup). No functional change vs 0.8.0; 0.8.0's tarball is byte-intact but
|
|
1032
|
+
its OIDC provenance pin moved with the history rewrite, so a fresh tag + publish restores a
|
|
1033
|
+
verifiable chain. Rotation: package.json + lock roots + plugin marker + scan UA + README pins +
|
|
1034
|
+
this entry.
|
|
1035
|
+
|
|
830
1036
|
### 0.8.0 (2026-09-30)
|
|
831
1037
|
- Add: **`border opencode inspect [--json]`** — the load-surface self-audit becomes a gate-owned
|
|
832
1038
|
surface: six read-only aspects (cache self-consistency, spec freshness vs pin/registry, identity
|
|
@@ -868,7 +1074,7 @@ which is now a thin consumer of `inspect --json` with the rc contract unchanged
|
|
|
868
1074
|
|
|
869
1075
|
### 0.6.0 (2026-09-25)
|
|
870
1076
|
- Add: **the exfil lens** — five rules (`exfil-rfc1918`, `exfil-ssh-target`,
|
|
871
|
-
`exfil-home-path`, `exfil-cred-location`, `exfil-host-profile`) that flag
|
|
1077
|
+
`exfil-home-path`, `exfil-cred-location`, `exfil-host-profile`, `exfil-machine-binding`) that flag
|
|
872
1078
|
exfil-shaped literals on about-to-be-public surfaces, the gap classic secret
|
|
873
1079
|
scanners leave open because an internal IP is not a credential. The observed
|
|
874
1080
|
severity of every (rule × facet × channel) cell lives in one machine-readable
|
|
@@ -1065,4 +1271,4 @@ border 是一个 fail-closed(失败即拦截)的推送前门禁 CLI:`npm install
|
|
|
1065
1271
|
|
|
1066
1272
|
OpenCode 插件适配层(0.5.0):零手工装载两条路线——主路是在 `opencode.jsonc` 声明 `"plugin": ["border-customs@0.5.0"]`(建议钉具体版本,@latest 每次冷启动都打注册表),opencode 自行下载构件,插件入口走 `exports["./server"]`;插件自带 CLI 解析,顺序为 `BORDER_BIN` 覆盖(独占,坏了就报错、绝不静默回退)> 包内同侧 `dist/index.js`(shebang 直接执行,不可执行则回退 PATH 上的 `node` 承载)> PATH 上的 `border`,免全局安装、免 PATH 配置;0.5.1 起任何候选都必须先过**身份握手**(以 `--help` 应答出 border 横幅与 `usage: border` 才算数),通过之前绝不投递用户 argv,握手不过即 cannot-answer exit 2、不运行任何其他二进制——0.5.0 曾用 `process.execPath` 承载包内 dist,而在插件宿主里 execPath 是 opencode 本体,边检门被静默冒充为宿主(2026-09-22 事故,见 0.5.1 变更日志)。备路是 `border opencode install|status|uninstall` 文件投递,落盘 `$XDG_CONFIG_HOME/opencode` 下的 `plugins/border.ts` 与 `commands/border.md`(无 XDG 回退 HOME/.config):受管文件凭 marker 行识别身份,同名外人文件 exit 2 拒绝、绝不覆盖,卸载只删自己带 marker 的文件,字节相同即 `up to date`。装载面只有一个 `border` 工具(argv-only 生成、无 shell、300 秒与每流 64 KiB 上限,命令闭集 check/push/status/llm-request/llm-ingest/scan/roundtrip/exfil/--help)与一条 `/border` 斜杠命令(config 钩子以 `??=` 自注册,用户同名文件命令保持权威)。边界如实说:allowlist 是 UX/防呆而非安全边界——插件进程与 opencode 进程同权,门禁的牙齿在 CLI 与账本,不在适配层;`push --yes` 按设计留在终端人审,工具端机械拒绝,会话内裸 push 本就是 CLI 合同的 DRY-RUN。装载验证走 opt-in 探针 `BORDER_OPENCODE_PROBE=1`(临时 HOME/XDG 起 `opencode serve` 对照组,断言 `border` 工具 id 与恰好一条 border 命令,重复即 FAIL;插件加载失败是静默的,只认 HTTP 响应不认退出码)。两条路线装载后都需重启 opencode:工具与命令在启动时扫描。
|
|
1067
1273
|
|
|
1068
|
-
外泄透镜(0.6.0):经典密钥扫描器看不见内网 IP、`user@内网主机`、家目录、凭据文件位置、主机画像这类"外泄形状"字面量——它们不是密钥,却如实描述公网仓库身后的私网。0.6.0 加五条规则 `exfil-rfc1918/exfil-ssh-target/exfil-home-path/exfil-cred-location/exfil-host-profile`,每个(规则×分面×通道)格子的**观察严重度**只有单一事实源 `src/exfil/severity.ts`:树面 HIGH 族走双生规则(gitleaks TOML + secretlint pattern,id 逐字节相等、三通道判决对等性有测试钉死),因两引擎都没有 HIGH 档而如实记为 CRITICAL(不硬掰,如实表);树面 MEDIUM 族走原生腿;提交消息面原生独占、id 带 `:message` 后缀,allow 条目按 id 精确匹配、跨面豁免在结构上不可能;tag note 分面归既有引擎腿 `tag-message-secret`,核心绝不重复发。规则源文件并入 rulesHash,改一个匹配器即令全部缓存 PASS 失效。独立只读入口 `border exfil <rev|url>`(默认树尖,`--deep` 补全史 blob 与全部可达提交消息;URL 模式落临时克隆、退出即销毁)。push 之后新增**落地核验**:ls-remote 复读远端尖、必要时精确 fetch 该对象,用同一套机制扫已公开树——干净则追加 `t:"landing"` 账本行并保留原退出码;有阻断发现则 exit 1 并明说 ALREADY PUBLIC;border 只检测、从不抹除;远端不可观测则 exit 2、一条记录都不造(没核验就没事实),已执行的 push 记录原样矗立——账本 append-only
|
|
1274
|
+
外泄透镜(0.6.0):经典密钥扫描器看不见内网 IP、`user@内网主机`、家目录、凭据文件位置、主机画像这类"外泄形状"字面量——它们不是密钥,却如实描述公网仓库身后的私网。0.6.0 加五条规则 `exfil-rfc1918/exfil-ssh-target/exfil-home-path/exfil-cred-location/exfil-host-profile`,每个(规则×分面×通道)格子的**观察严重度**只有单一事实源 `src/exfil/severity.ts`:树面 HIGH 族走双生规则(gitleaks TOML + secretlint pattern,id 逐字节相等、三通道判决对等性有测试钉死),因两引擎都没有 HIGH 档而如实记为 CRITICAL(不硬掰,如实表);树面 MEDIUM 族走原生腿;提交消息面原生独占、id 带 `:message` 后缀,allow 条目按 id 精确匹配、跨面豁免在结构上不可能;tag note 分面归既有引擎腿 `tag-message-secret`,核心绝不重复发。规则源文件并入 rulesHash,改一个匹配器即令全部缓存 PASS 失效。独立只读入口 `border exfil <rev|url>`(默认树尖,`--deep` 补全史 blob 与全部可达提交消息;URL 模式落临时克隆、退出即销毁)。push 之后新增**落地核验**:ls-remote 复读远端尖、必要时精确 fetch 该对象,用同一套机制扫已公开树——干净则追加 `t:"landing"` 账本行并保留原退出码;有阻断发现则 exit 1 并明说 ALREADY PUBLIC;border 只检测、从不抹除;远端不可观测则 exit 2、一条记录都不造(没核验就没事实),已执行的 push 记录原样矗立——账本 append-only,任何情况下不改写历史。2026-10-04 补上透镜对等:check 流水线在原生规则之后、注册表预检之前多跑一条**树尖腿**,用与落地核验完全同一套 scanTipTree 机制预扫本地 HEAD(push 将传输的那棵树),历史与工作面腿看不见的"只在树尖存在"内容(如合并提交解析引入、随后从磁盘删除的文件)从此无处藏;树尖发现汇入 allow 前合并点,按 rule+file+digest 与所有发现同权豁免——落地核验同样消费 cfg allow 并把每一次豁免大声列出(规则、条目号、条数),绝不沉默;绿色交接单与绿色落地双向一致。ledger key 折入透镜身份,旧一代(无树尖腿)的缓存 PASS 不再为新透镜背书,升级后强制一次全新全扫。树尖腿 git 故障即 exit 2,没有静默清白。测试语料遵守夹具纪律:10.200.0.0/16 为预留夹具段(树面全通道静默、双生规则原生镜像该豁免,消息面例外照红),其余泄漏形状一律由 `assembleOctet`/`assembleHost` 运行时拼装,签出字节里没有完整泄漏 token——border 的透镜对自己的仓库绿,红故事照常武装。真值通道 `BORDER_EXFIL_TRUTH=1` 为 opt-in,红锚点带生命周期条款:上游作者清理后按设计转绿、留档不判败。
|
|
@@ -176,4 +176,4 @@ export function matchSshTarget(text: string, o: RuleOptions): RuleHit[] {
|
|
|
176
176
|
// matchHomePath / matchCredLocation / matchHostProfile live in rulesPath.ts
|
|
177
177
|
// and are re-exported here so every consumer keeps importing the five
|
|
178
178
|
// predicates from one module. Type-only imports keep that pair acyclic.
|
|
179
|
-
export { matchCredLocation, matchHomePath, matchHostProfile } from "./rulesPath.ts";
|
|
179
|
+
export { matchCredLocation, matchHomePath, matchHostProfile, matchMachineBinding } from "./rulesPath.ts";
|
|
@@ -110,3 +110,31 @@ export function matchHostProfile(text: string, _o: RuleOptions): RuleHit[] {
|
|
|
110
110
|
flush();
|
|
111
111
|
return hits;
|
|
112
112
|
}
|
|
113
|
+
|
|
114
|
+
// ---------------------------------------------------------------- exfil-machine-binding
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* L-MACHINE-LOCAL (owner ruling 2026-10-01; rule born 2026-10-02 after a
|
|
118
|
+
* vendor model id sat in another repo's portable README+code default for six
|
|
119
|
+
* releases unnoticed). Vendor inference-model identifiers pinned on portable
|
|
120
|
+
* surfaces couple a package to ONE operator's device authorization. Context
|
|
121
|
+
* gate: the line must ALSO speak of model/default/provider — prose mentions
|
|
122
|
+
* of a model name stay green (location, not vocabulary — mirrors the
|
|
123
|
+
* cred-location "position, not value" discipline).
|
|
124
|
+
* Self-stealth: literals are built in fragments so THIS file never trips
|
|
125
|
+
* its own regex.
|
|
126
|
+
*/
|
|
127
|
+
const BINDING_TOKEN = String.raw`\b(?:q` + "wen" + String.raw`|deep` + "seek" + String.raw`|kimi|g` + "lm" + String.raw`|ernie|dou` + "bao" + String.raw`|hunyuan|sp` + "ark" + String.raw`)[-\.\d][\w.-]*\b`;
|
|
128
|
+
const BINDING_VENDOR_RE = new RegExp(BINDING_TOKEN, "gi");
|
|
129
|
+
const BINDING_CONTEXT_RE = /(?:model|模型|default|默认|provider)/i;
|
|
130
|
+
|
|
131
|
+
export function matchMachineBinding(text: string, _o: RuleOptions): RuleHit[] {
|
|
132
|
+
const hits: RuleHit[] = [];
|
|
133
|
+
text.split("\n").forEach((body, idx) => {
|
|
134
|
+
if (!BINDING_CONTEXT_RE.test(body)) return;
|
|
135
|
+
for (const m of body.matchAll(BINDING_VENDOR_RE)) {
|
|
136
|
+
hits.push({ rule: "exfil-machine-binding", matched: m[0] ?? "", line: idx + 1, message: "Machine binding: a vendor inference-model id pinned next to model/default/provider context — device authorization belongs in operator-local config, not portable surfaces (L-MACHINE-LOCAL)." });
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
return hits;
|
|
140
|
+
}
|
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
import {
|
|
41
41
|
matchCredLocation,
|
|
42
42
|
matchHomePath,
|
|
43
|
+
matchMachineBinding,
|
|
43
44
|
matchHostProfile,
|
|
44
45
|
matchRfc1918,
|
|
45
46
|
matchSshTarget,
|
|
@@ -99,6 +100,7 @@ const PREDICATES: Readonly<Record<ExfilRuleId, (text: string, o: RuleOptions) =>
|
|
|
99
100
|
"exfil-home-path": matchHomePath,
|
|
100
101
|
"exfil-cred-location": matchCredLocation,
|
|
101
102
|
"exfil-host-profile": matchHostProfile,
|
|
103
|
+
"exfil-machine-binding": matchMachineBinding,
|
|
102
104
|
};
|
|
103
105
|
|
|
104
106
|
/**
|
|
@@ -44,13 +44,14 @@
|
|
|
44
44
|
export const EXFIL_SEVERITIES = ["INFO", "LOW", "MEDIUM", "HIGH", "CRITICAL"] as const;
|
|
45
45
|
export type ExfilSeverity = (typeof EXFIL_SEVERITIES)[number];
|
|
46
46
|
|
|
47
|
-
/** The
|
|
47
|
+
/** The exfil-lens rule ids, verbatim; twins must ship these ids byte-equal (T2 guard). */
|
|
48
48
|
export const EXFIL_RULE_IDS = [
|
|
49
49
|
"exfil-rfc1918",
|
|
50
50
|
"exfil-ssh-target",
|
|
51
51
|
"exfil-home-path",
|
|
52
52
|
"exfil-cred-location",
|
|
53
53
|
"exfil-host-profile",
|
|
54
|
+
"exfil-machine-binding",
|
|
54
55
|
] as const;
|
|
55
56
|
export type ExfilRuleId = (typeof EXFIL_RULE_IDS)[number];
|
|
56
57
|
|
|
@@ -71,6 +72,7 @@ export const EXFIL_TABLE_SEVERITY: Readonly<Record<ExfilRuleId, ExfilSeverity>>
|
|
|
71
72
|
"exfil-home-path": "MEDIUM",
|
|
72
73
|
"exfil-cred-location": "MEDIUM",
|
|
73
74
|
"exfil-host-profile": "MEDIUM",
|
|
75
|
+
"exfil-machine-binding": "MEDIUM",
|
|
74
76
|
};
|
|
75
77
|
|
|
76
78
|
/** HIGH family: the two identity-carrying location rules; tree face belongs to the twins. */
|
|
@@ -85,7 +87,7 @@ export const EXFIL_HIGH_FAMILY: readonly ExfilRuleId[] = ["exfil-rfc1918", "exfi
|
|
|
85
87
|
*/
|
|
86
88
|
export const EXFIL_SECRET_VALUE_RULES: readonly ExfilRuleId[] = EXFIL_HIGH_FAMILY;
|
|
87
89
|
/** MEDIUM family: privacy-shape rules; native carries them on both scanned facets. */
|
|
88
|
-
export const EXFIL_MEDIUM_FAMILY: readonly ExfilRuleId[] = ["exfil-home-path", "exfil-cred-location", "exfil-host-profile"];
|
|
90
|
+
export const EXFIL_MEDIUM_FAMILY: readonly ExfilRuleId[] = ["exfil-home-path", "exfil-cred-location", "exfil-host-profile", "exfil-machine-binding"];
|
|
89
91
|
|
|
90
92
|
/** null = FORBIDDEN cell: that channel must never emit that rule on that facet. */
|
|
91
93
|
export type ObservedSeverity = ExfilSeverity | null;
|
|
@@ -96,6 +98,7 @@ const TWIN_HIGH: Readonly<Record<ExfilRuleId, ObservedSeverity>> = {
|
|
|
96
98
|
"exfil-home-path": null,
|
|
97
99
|
"exfil-cred-location": null,
|
|
98
100
|
"exfil-host-profile": null,
|
|
101
|
+
"exfil-machine-binding": null,
|
|
99
102
|
};
|
|
100
103
|
const NULL_ROW: Readonly<Record<ExfilRuleId, ObservedSeverity>> = {
|
|
101
104
|
"exfil-rfc1918": null,
|
|
@@ -103,13 +106,14 @@ const NULL_ROW: Readonly<Record<ExfilRuleId, ObservedSeverity>> = {
|
|
|
103
106
|
"exfil-home-path": null,
|
|
104
107
|
"exfil-cred-location": null,
|
|
105
108
|
"exfil-host-profile": null,
|
|
109
|
+
"exfil-machine-binding": null,
|
|
106
110
|
};
|
|
107
111
|
|
|
108
112
|
/** The full (facet × channel × rule) truth table — the plan's 分面契约, machine-readable. */
|
|
109
113
|
export const EXFIL_MATRIX: Readonly<Record<ExfilFacet, Readonly<Record<ExfilChannel, Readonly<Record<ExfilRuleId, ObservedSeverity>>>>>> = {
|
|
110
114
|
tree: {
|
|
111
115
|
// native tree leg = MEDIUM family补发 only (v2 double-emission revoked).
|
|
112
|
-
native: { ...NULL_ROW, "exfil-home-path": "MEDIUM", "exfil-cred-location": "MEDIUM", "exfil-host-profile": "MEDIUM" },
|
|
116
|
+
native: { ...NULL_ROW, "exfil-home-path": "MEDIUM", "exfil-cred-location": "MEDIUM", "exfil-host-profile": "MEDIUM", "exfil-machine-binding": "MEDIUM" },
|
|
113
117
|
// gitleaks.ts:81 hardcodes CRITICAL for every ingested finding.
|
|
114
118
|
"gitleaks-twin": TWIN_HIGH,
|
|
115
119
|
// secretlint.ts:299-309 has no HIGH rung — error maps to CRITICAL as-is.
|
|
@@ -1,3 +1,27 @@
|
|
|
1
|
+
# Vendored from gitleaks v8.30.1 (assets/default_config.toml), MIT License.
|
|
2
|
+
#
|
|
3
|
+
# MIT License
|
|
4
|
+
#
|
|
5
|
+
# Copyright (c) 2019 Zachary Rice
|
|
6
|
+
#
|
|
7
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
8
|
+
# of this software and associated documentation files (the "Software"), to deal
|
|
9
|
+
# in the Software without restriction, including without limitation the rights
|
|
10
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
11
|
+
# copies of the Software, and to permit persons to whom the Software is
|
|
12
|
+
# furnished to do so, subject to the following conditions:
|
|
13
|
+
#
|
|
14
|
+
# The above copyright notice and this permission notice shall be included in all
|
|
15
|
+
# copies or substantial portions of the Software.
|
|
16
|
+
#
|
|
17
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
18
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
19
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
20
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
21
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
22
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
23
|
+
# SOFTWARE.
|
|
24
|
+
|
|
1
25
|
# This file has been auto-generated. Do not edit manually.
|
|
2
26
|
# If you would like to contribute new rules, please use
|
|
3
27
|
# cmd/generate/config/main.go and follow the contributing guidelines
|