border-customs 0.1.0
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/LICENSE +21 -0
- package/README.md +371 -0
- package/dist/assets/gitleaks-defaults-v8.30.1.toml +3209 -0
- package/dist/assets/package-lock.json +5699 -0
- package/dist/assets/prompts/llm-review.md +69 -0
- package/dist/index.js +41730 -0
- package/package.json +40 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Border-Customs contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
# border
|
|
2
|
+
|
|
3
|
+
A fail-closed pre-push gate for git, npm, and PyPI. It scans your repo for secrets and
|
|
4
|
+
supply-chain risk, then refuses to let anything leave the machine unless a fresh, unbroken
|
|
5
|
+
check passed for exactly the state you are about to publish.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g border-customs
|
|
9
|
+
cd /path/to/your/repo
|
|
10
|
+
border check
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Requires Node >= 22 and the `gitleaks` binary on `PATH` (border vendors the rule config for
|
|
14
|
+
gitleaks 8.30.1; the binary itself is yours to install, same as `git`). secretlint runs
|
|
15
|
+
in-process as a bundled dependency. Optional engines are listed in
|
|
16
|
+
[Configuration](#configuration).
|
|
17
|
+
|
|
18
|
+
## The problem it solves
|
|
19
|
+
|
|
20
|
+
The usual secret workflow is leak-then-repent: `git push`, notice the AWS key in the diff
|
|
21
|
+
review (or not), then `git filter-repo`, force-push, revoke the credential, and email the
|
|
22
|
+
security list. Post-hoc scrubbing fails for two mechanical reasons. Force-pushing a public
|
|
23
|
+
branch is history rewriting: clones, forks, CI caches, and package mirrors already hold the
|
|
24
|
+
bad objects. And published package versions are effectively forever: npm's unpublish policy
|
|
25
|
+
forbids reusing a `name@version` once it was ever used, and PyPI yanking is non-destructive.
|
|
26
|
+
The only cheap fix is to not cross the boundary, and the only boundary that matters is the
|
|
27
|
+
one between this machine and the world.
|
|
28
|
+
|
|
29
|
+
Git's own `pre-commit` and `pre-push` hooks help, but they gate the *command*, not the
|
|
30
|
+
*state*, and they trust whatever ran last. border inverts that: a push is only allowed when
|
|
31
|
+
a check passed for the exact fingerprint of what would be pushed, where "what would be
|
|
32
|
+
pushed" includes the six things people forget:
|
|
33
|
+
|
|
34
|
+
- **Untracked and dirty files are inputs too.** History scanning alone misses the `.env`
|
|
35
|
+
that was never committed but will ride along in a `git add -A`. border fingerprints the
|
|
36
|
+
full `git status --porcelain` digest and scans the working tree directly.
|
|
37
|
+
- **Deleted-but-pushed blobs.** A secret removed in a later commit still ships in the
|
|
38
|
+
history of the ref being pushed. The history leg scans everything the push would
|
|
39
|
+
transmit, not just HEAD.
|
|
40
|
+
- **Archives.** Tarballs and zips inside the repo are unpacked and scanned with
|
|
41
|
+
`<archive>!<inner-path>` attribution.
|
|
42
|
+
- **The published bytes, not the source tree.** `npm pack` and `python -m build` rewrite
|
|
43
|
+
what ships. border packs/builds once into `.border/dist/`, scans those exact bytes, and
|
|
44
|
+
re-hashes them again at publish time.
|
|
45
|
+
- **The registry.** Pushing `package.json` at version `1.2.3` when `1.2.3` already exists
|
|
46
|
+
on the registry is a supply-chain event (silent stale artifact, or squatted name). border
|
|
47
|
+
fails the check on "version exists, bump required" and on foreign ownership of your name.
|
|
48
|
+
- **Identity.** A repo whose pushed commits are authored by an identity outside your
|
|
49
|
+
allow-list is on fire, and `PASS` from an earlier state does not vouch for it at push
|
|
50
|
+
time; border re-scans identity on the transmit set before every real push.
|
|
51
|
+
|
|
52
|
+
## Principle
|
|
53
|
+
|
|
54
|
+
One sentence: **a PASS is a statement about a fingerprint, not about the past.**
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
checkKey = sha256( headSha
|
|
58
|
+
⊕ porcelainDigest # every tracked/untracked/staged byte-change
|
|
59
|
+
⊕ rulesHash # config digest + vendored rules + engine versions + prompt template
|
|
60
|
+
⊕ exposureSet # sanitized remote URLs + npm/pypi name@version
|
|
61
|
+
⊕ refSet # branch being pushed + every local tag
|
|
62
|
+
⊕ effectiveTargets ) # git, npm, pypi as configured for this run
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Move *any* of those six and the key moves, the stored PASS no longer covers the state, and
|
|
66
|
+
border makes you re-check. Committing, editing an untracked file, renaming a remote,
|
|
67
|
+
bumping a version, adding a tag, editing `border.yaml`, or an engine version bump all
|
|
68
|
+
invalidate. There is no time-based expiry and no "looks the same": the check either covers
|
|
69
|
+
this exact state or it does not.
|
|
70
|
+
|
|
71
|
+
Everything else in the design follows from making that statement unbreakable: the ledger is
|
|
72
|
+
append-only and corruption-tolerant, degraded tools poison the verdict instead of hiding
|
|
73
|
+
it, and the two registry-facing legs (pre-flight and publish) both have to agree.
|
|
74
|
+
|
|
75
|
+
## Architecture: what `border check` runs, in order
|
|
76
|
+
|
|
77
|
+
1. **Config load.** `border.yaml` parsed with zod in strict mode. Unknown keys, bad types,
|
|
78
|
+
or unset `${ENV}` placeholders are hard errors (exit 2), never defaults. If no config
|
|
79
|
+
exists but git remotes do, border infers a `git`-only config and says so on stderr. A
|
|
80
|
+
hidden local overlay `.border/config.local.yaml` is deep-merged for machine-local pins.
|
|
81
|
+
2. **Engine policy probe.** Every required engine (default `gitleaks` + `secretlint`,
|
|
82
|
+
overridable with `--require-engine`) must answer a version probe. A missing or broken
|
|
83
|
+
probe yields a `DEGRADED-ENGINE` CRITICAL finding and forces **exit 2 with the verdict
|
|
84
|
+
marked UNTRUSTWORTHY**: border refuses to emit a PASS it cannot mechanically justify.
|
|
85
|
+
An engine whose version output cannot be parsed also exits 2 rather than assuming a
|
|
86
|
+
current tool.
|
|
87
|
+
3. **Hostile-config guard.** Before trusting any scan, the HEAD tree is checked for a
|
|
88
|
+
committed `.gitleaksignore` or `.gitleaks.toml`. Those files make gitleaks itself
|
|
89
|
+
obey the repo it should be auditing (verified empirically: one committed ignore file
|
|
90
|
+
turned 1 finding into 0). Their mere presence is a CRITICAL `repo-self-ignores-findings`
|
|
91
|
+
finding, because refusing to trust them is the only fail-closed option. border always
|
|
92
|
+
passes its own vendored config explicitly, so repo-shipped rules cannot replace it.
|
|
93
|
+
4. **gitleaks legs.** History scan over the refs a push would transmit (secrets in
|
|
94
|
+
commits unreachable from HEAD still count), working-tree directory scan (archive depth
|
|
95
|
+
2, plus border's own extractor for formats gitleaks misses such as `.tgz`), and a tag
|
|
96
|
+
*message* leg (annotation bodies are pushed with the tag and are not covered by file
|
|
97
|
+
scans). Findings inside extracted archives are re-attributed to `archive!<inner>`.
|
|
98
|
+
5. **secretlint leg.** Runs in-process on the git-listed file set with the recommended
|
|
99
|
+
preset, `no-homedir`, `no-dotenv`, plus `border.yaml` rules turned into patterns
|
|
100
|
+
(hosts, IPs, paths). The upstream AWS access-key ID rule ships force-enabled, and
|
|
101
|
+
every secretlint hit's echoed raw value is stripped from messages before persistence.
|
|
102
|
+
6. **Native rules.** AI-session artifact detection on a *closed* matcher list
|
|
103
|
+
(`.omo/**`, `**/transcripts/**`, `*.session.jsonl`, `opencode.json(c)`, `.opencode/**`,
|
|
104
|
+
env files except `.env.example`/`.env.sample`, junk like `probe*`/`*.rej`/
|
|
105
|
+
`node_modules/**`, plus your `rules.pathPatterns` through the same compiler). Identity
|
|
106
|
+
allow-listing over commit authors, merge committers, and annotated-tag taggers, checked
|
|
107
|
+
against the history of every ref *and* against the object set a remote does not already
|
|
108
|
+
have (the transmit set). Oversized-file, checked-in-binary, and notebook-output rules
|
|
109
|
+
round it out.
|
|
110
|
+
7. **Registry pre-flight** (npm/PyPI targets only). Three outcomes per target: version
|
|
111
|
+
already published ⇒ CRITICAL `version-exists` ("bump version required"); name owned by
|
|
112
|
+
someone outside your `rules.authors` allow-list ⇒ CRITICAL `name-foreign-owner`;
|
|
113
|
+
ambiguous ownership signals ⇒ also CRITICAL, because guessing wrong is how squats ship.
|
|
114
|
+
Critically, **silence never means absent**: an empty stdout from `npm view`, a non-200
|
|
115
|
+
non-404 PyPI response, a timeout, or unparseable JSON all fail closed as exit 2. An
|
|
116
|
+
unreachable registry blocks the push instead of letting it run blind.
|
|
117
|
+
8. **Artifact stage.** If npm/PyPI targets are configured, packages are built **once** into
|
|
118
|
+
`.border/dist/` (`npm pack --ignore-scripts`, `python -m build --no-isolation`), that
|
|
119
|
+
exact byte-stream is scanned (gitleaks + secretlint over the extracted contents),
|
|
120
|
+
manifest-diffed (lifecycle `preinstall`/`install`/`postinstall`/`prepare` hooks are
|
|
121
|
+
CRITICAL; entries outside the `files` whitelist are HIGH; PyPI sdists get a
|
|
122
|
+
`sdist-unexpected-file` HIGH because setuptools builds from the working tree and is
|
|
123
|
+
`.gitignore`-blind), checked with `publint` / `twine check --strict`, and recorded as
|
|
124
|
+
`{file, sha256, bytes}` in the ledger.
|
|
125
|
+
At publish time the *same bytes* are re-hashed: any mismatch is exit 2, before the wire.
|
|
126
|
+
9. **Report.** Findings carry `valueDigest` (sha256 of the matched value) and a masked
|
|
127
|
+
snippet: fully blocked for short values, else `first4…last4`. Raw secret bytes never
|
|
128
|
+
leave process memory. A per-run `TextSanitizer` holds the digest-to-value registry and
|
|
129
|
+
replaces every flagged literal in *all* rendered text, including agent-written strings in
|
|
130
|
+
the LLM layer, so a secret cannot reappear inside a message about itself. `report.json`
|
|
131
|
+
(canonical, byte-stable for machines) and `report.md` land under `.border/runs/`.
|
|
132
|
+
10. **Ledger record.** A PASS is appended to `.border/ledger.jsonl` together with the key
|
|
133
|
+
and artifact digests. Degraded and NO-OP runs can never write a PASS.
|
|
134
|
+
|
|
135
|
+
```bash
|
|
136
|
+
$ border check # full scan, ~seconds on a mid-size repo
|
|
137
|
+
$ border check # identical state: no re-scan
|
|
138
|
+
SKIP 35ffb3cb — PASS 2026-09-04T18:11:44.976Z report .border/runs/35ffb3cb-.../report.json
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
The **skip-ledger** is what makes the gate cheap enough to run on every push: a repeat
|
|
142
|
+
`border check` on an unchanged fingerprint replays the recorded verdict in well under a
|
|
143
|
+
second. The replay is not blind trust. Before honoring a skip border re-derives the live
|
|
144
|
+
fingerprint (including engine probes), refuses skips recorded in the other LLM mode, and
|
|
145
|
+
re-packs npm artifacts to prove the recorded tarball digests still match fresh byte-for-byte
|
|
146
|
+
(a dirty tree, a drifted `package.json`, or a changed npm version all force a full re-check;
|
|
147
|
+
PyPI builds are not byte-reproducible, so their skip proof is head+porcelain equality, and
|
|
148
|
+
publish still re-hashes the exact dist files). A corrupt ledger line is dropped with a loud
|
|
149
|
+
warning, never a crash, and never a silently empty history.
|
|
150
|
+
|
|
151
|
+
## Architecture: `border push`
|
|
152
|
+
|
|
153
|
+
`border push` is a state machine over per-target states, recomputed from live git queries,
|
|
154
|
+
never from cache:
|
|
155
|
+
|
|
156
|
+
| State | Meaning |
|
|
157
|
+
| --- | --- |
|
|
158
|
+
| `PUSHED` | remote already holds every ref at the local value (checked via `git ls-remote`, full-ref compare, tags compared *peeled*) |
|
|
159
|
+
| `PENDING` | this target needs to move, and the current fingerprint has a PASS behind it |
|
|
160
|
+
| `BLOCKED` | no PASS for this state (run `border check`), or a registry finding says stop |
|
|
161
|
+
|
|
162
|
+
- Bare `border push` is a **DRY-RUN**: it prints the exact `git push --dry-run` /
|
|
163
|
+
`npm publish` / `twine upload` lines it would run, refuses to act, and exits with the
|
|
164
|
+
gate's verdict. `--yes` is what executes.
|
|
165
|
+
- Multi-remote git push is **all-or-nothing before anything moves**: every remote × every
|
|
166
|
+
ref must be a fast-forward, checked up front. If any remote diverged, border prints
|
|
167
|
+
`DIVERGED` with the offending shas and pushes nothing. **It never force-pushes; there is
|
|
168
|
+
no flag for it.**
|
|
169
|
+
- Registry legs go through the same gate as push: a PASS record for the current key, then
|
|
170
|
+
the artifact re-hash match against `.border/dist/`, then an immediate version-exists
|
|
171
|
+
re-probe (the registry is allowed to change between check and publish), then the upload
|
|
172
|
+
with `stdio: inherit` so `npm`/`twine` OTP and credential prompts reach you untouched.
|
|
173
|
+
border never reads, stores, or handles registry tokens. Published versions are not
|
|
174
|
+
retried on failure because a half-published version can never be republished.
|
|
175
|
+
- If one remote of several succeeded before a failure, border says so explicitly (PARTIAL)
|
|
176
|
+
and exits 1; a rerun of `border push --yes` picks up only the still-PENDING targets.
|
|
177
|
+
|
|
178
|
+
## Architecture: the LLM layer (optional, agent-executed)
|
|
179
|
+
|
|
180
|
+
border itself **never calls any model API**. The `--llm` layer is a two-file handoff with
|
|
181
|
+
the operator's own agent (Claude, Codex, a human with a chat tab):
|
|
182
|
+
|
|
183
|
+
```bash
|
|
184
|
+
border check # must pass for the current state first
|
|
185
|
+
border llm-request # writes .border/runs/<key8>-.../llm-request.json
|
|
186
|
+
# → your agent reads the bundle, follows the embedded prompt template,
|
|
187
|
+
# and authors findings.json (a strict array; [] is a valid PASS statement)
|
|
188
|
+
border llm-ingest findings.json # validates, re-scrubs, recomputes the verdict
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
The request bundle is a *masked* review context: the diff versus the remote tip (or a
|
|
192
|
+
stated full-tree diff on first push, truncated over 10 MiB with an explicit marker), the
|
|
193
|
+
deterministic findings, artifact digests, and the review prompt with its sha256. Values the
|
|
194
|
+
deterministic engines flagged appear only as `[REDACTED:<sha8>]`. The ingest side validates
|
|
195
|
+
every agent finding against the schema, rejects any finding whose `path` is not in the
|
|
196
|
+
bundle (an agent inventing locations is a hard error, exit 2), forces `engine: "agent"`,
|
|
197
|
+
computes digests itself rather than trusting agent-supplied ones, re-runs the sanitizer over
|
|
198
|
+
agent free text, and records an `llm: true` PASS that plain checks cannot ride and vice
|
|
199
|
+
versa. The residual risk is stated in the bundle itself: masking only covers values the
|
|
200
|
+
deterministic layers already found, so an undiscovered secret in diff context *can* reach
|
|
201
|
+
your LLM endpoint. That is the accepted boundary of this optional layer; the deterministic
|
|
202
|
+
layers remain the actual gate.
|
|
203
|
+
|
|
204
|
+
## Commands
|
|
205
|
+
|
|
206
|
+
```
|
|
207
|
+
border <command> [options]
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
| Command | Purpose |
|
|
211
|
+
| --- | --- |
|
|
212
|
+
| `check` | run the secret + supply-chain gate on the pending scope |
|
|
213
|
+
| `push` | gate-verified push; DRY-RUN unless `--yes` |
|
|
214
|
+
| `status` | newest gate records per target |
|
|
215
|
+
| `llm-request` | emit the masked review bundle for LLM-authored commits |
|
|
216
|
+
| `llm-ingest <findings.json>` | validate agent findings and record the combined verdict |
|
|
217
|
+
|
|
218
|
+
Every subcommand accepts the same global flags (verified against `border <cmd> --help`):
|
|
219
|
+
|
|
220
|
+
| Flag | Effect |
|
|
221
|
+
| --- | --- |
|
|
222
|
+
| `--config <path>` | config file; default `./border.yaml` |
|
|
223
|
+
| `--targets <list>` | comma-separated `git,npm,pypi` subset restricting this run's scope; a named target that is not configured is exit 2 |
|
|
224
|
+
| `--force` | ignore the skip-ledger, re-run the full check |
|
|
225
|
+
| `--yes` | execute mutations (push only); without it a push is always DRY-RUN |
|
|
226
|
+
| `--require-engine <list>` | replaces the required-engine set from config; unknown or unprobeable names degrade the run (exit 2) |
|
|
227
|
+
| `--llm` | opt this check into the LLM review layer (a plain check can never satisfy an llm-recorded skip) |
|
|
228
|
+
| `--json` | machine-readable report on stdout (check) |
|
|
229
|
+
| `--help, -h` | usage table |
|
|
230
|
+
|
|
231
|
+
### Exit codes (the contract)
|
|
232
|
+
|
|
233
|
+
| Code | Meaning |
|
|
234
|
+
| --- | --- |
|
|
235
|
+
| `0` | PASS (or no-op): MEDIUM / LOW / INFO findings are allowed and listed |
|
|
236
|
+
| `1` | gate-blocked: HIGH or CRITICAL findings; or a push refused (BLOCKED targets); or a partial push |
|
|
237
|
+
| `2` | the gate could not answer: config error, missing/degraded engine, unreachable registry, malformed input, concurrent lock holder |
|
|
238
|
+
|
|
239
|
+
The `2` class matters as much as `1`: a tool error never exits 0, and no exit-0 run ever
|
|
240
|
+
rests on a leg that silently skipped. Engine exit codes are translated against a closed
|
|
241
|
+
matrix (`gitleaks` 0/1, `trufflehog` 0/183, `secretlint` 0/1); *anything* else, including a
|
|
242
|
+
126 from gitleaks, is exit 2, never "clean".
|
|
243
|
+
|
|
244
|
+
## Configuration
|
|
245
|
+
|
|
246
|
+
Everything border can be told lives in `border.yaml` at the repo root. Unknown keys are
|
|
247
|
+
rejected at load (a typo in a gate config is a gate you did not ask for). `${VAR}`
|
|
248
|
+
expansion applies only to remote URLs and registry/repository values, and an unset variable
|
|
249
|
+
is a hard error, never an empty string.
|
|
250
|
+
|
|
251
|
+
Minimal, a git-only repo:
|
|
252
|
+
|
|
253
|
+
```yaml
|
|
254
|
+
version: 1
|
|
255
|
+
targets:
|
|
256
|
+
git:
|
|
257
|
+
remotes:
|
|
258
|
+
- name: origin
|
|
259
|
+
url: git@github.com:acme/widgets.git
|
|
260
|
+
rules:
|
|
261
|
+
authors:
|
|
262
|
+
emails: [devs@acme.example] # identity allow-list: commits authored by
|
|
263
|
+
names: [Acme Dev] # anyone else are CRITICAL findings
|
|
264
|
+
hosts: [] # extra detection patterns (internal hostnames…)
|
|
265
|
+
ips: []
|
|
266
|
+
pathPatterns: []
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
`rules` is required by design: a gate with silent defaults is a gate you did not ask for.
|
|
270
|
+
|
|
271
|
+
A repo that publishes a package, with the full surface:
|
|
272
|
+
|
|
273
|
+
```yaml
|
|
274
|
+
version: 1
|
|
275
|
+
targets:
|
|
276
|
+
git:
|
|
277
|
+
remotes:
|
|
278
|
+
- name: origin
|
|
279
|
+
url: https://github.com/acme/widgets.git
|
|
280
|
+
npm:
|
|
281
|
+
registry: https://registry.npmjs.org # optional; any npm-protocol registry works
|
|
282
|
+
pypi:
|
|
283
|
+
repository: https://pypi.org # optional; TestPyPI or private indexes work
|
|
284
|
+
rules:
|
|
285
|
+
authors: # identity allow-list
|
|
286
|
+
emails: [devs@acme.example]
|
|
287
|
+
names: [Acme CI]
|
|
288
|
+
allowBots: true
|
|
289
|
+
hosts: ["git.internal.acme.example"] # turned into detection patterns
|
|
290
|
+
ips: ["10.20.30.40"]
|
|
291
|
+
pathPatterns: ["/Users/*", "*.pfx"]
|
|
292
|
+
maxFileKB: 500 # oversized-file threshold (default)
|
|
293
|
+
allow: # enumerated suppressions, never blanket
|
|
294
|
+
- { rule: "junk-artifact", match: "*", file: "test/fixtures/**" }
|
|
295
|
+
engines:
|
|
296
|
+
require: [gitleaks, secretlint] # trufflehog: true adds the third-party engine
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
Notes that change behavior:
|
|
300
|
+
|
|
301
|
+
- `targets.git.remotes: []` (explicit, empty) is a *deliberate* repo-local scope: all
|
|
302
|
+
history/tree/identity/artifact legs still run, only the exposure set is empty. A
|
|
303
|
+
completely missing config with real remotes falls back to config-from-`git remote` with
|
|
304
|
+
a loud stderr warning.
|
|
305
|
+
- Every `allow` entry must be scoped (a `file` pin); `{rule: "*", match: "*"}` with no
|
|
306
|
+
file is rejected at load. Suppressed findings are enumerated in the report's
|
|
307
|
+
`allow-hits` section, so an exit 0 never hides what it hid.
|
|
308
|
+
- `rules.hosts/ips/pathPatterns` feed the same matcher pipeline as the built-ins, so your
|
|
309
|
+
patterns get the same archive attribution and the same allow-listing semantics.
|
|
310
|
+
|
|
311
|
+
## Integration
|
|
312
|
+
|
|
313
|
+
**Git hook.** The gate is one command, so the classic one-liner is exactly:
|
|
314
|
+
|
|
315
|
+
```bash
|
|
316
|
+
printf '#!/bin/sh\nexec border check --force\n' > .git/hooks/pre-push && chmod +x .git/hooks/pre-push
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
`check` exits nonzero whenever the push it would gate is not covered, so any push through
|
|
320
|
+
the hook must first pass. Drop `--force` to get sub-second skip-ledger replay instead of a
|
|
321
|
+
re-scan on every `git push`.
|
|
322
|
+
|
|
323
|
+
**CI.** `border check --json` emits the stable report object; gate on the exit code, parse
|
|
324
|
+
`verdict`/`counts` for dashboards. In CI you typically want `--force` on a cache-less
|
|
325
|
+
runner, plus your `gitleaks` binary provisioned in the image (a degraded run exits 2, which
|
|
326
|
+
most CI maps to red; that is the point).
|
|
327
|
+
|
|
328
|
+
**Agents.** `skills/border/SKILL.md` ships with the repo: it teaches an operator agent (the
|
|
329
|
+
same one that wrote the commit) how to run the five subcommands, how to produce a valid
|
|
330
|
+
`llm-ingest` findings file, and the two standing rules for agents driving pushes: never pass
|
|
331
|
+
`--yes` without a visible human go-ahead, and never push over an exit-2 gate.
|
|
332
|
+
|
|
333
|
+
## What this is not
|
|
334
|
+
|
|
335
|
+
- **Not a vault or secret manager.** It detects credentials on their way out; it does not
|
|
336
|
+
store, rotate, or inject them.
|
|
337
|
+
- **Not a history rewriter.** If a secret is already in a pushed commit, the fix
|
|
338
|
+
(`git filter-repo` plus credential rotation) is explicitly out of scope and named in the
|
|
339
|
+
finding text. border refuses rather than rewrites.
|
|
340
|
+
- **Not an escape hatch.** There is no force-push flag, no `--i-know-what-im-doing`, no
|
|
341
|
+
per-run expiry of a missing PASS. The refusal paths are the product. If a check cannot be
|
|
342
|
+
trusted (engine gone, registry unreachable), the result is stop, not proceed.
|
|
343
|
+
- **Not a replacement for review or branch protection.** It gates *this machine's* push
|
|
344
|
+
surface; server-side controls still own everything else.
|
|
345
|
+
|
|
346
|
+
## Security posture
|
|
347
|
+
|
|
348
|
+
- **Redaction is the default output channel.** Findings, reports, status lines, and agent
|
|
349
|
+
text all flow through masked snippets (`valueDigest` + `first4…last4`, full block for
|
|
350
|
+
short values) and the run-scoped sanitizer; raw matched bytes never leave memory.
|
|
351
|
+
- **Fail-closed doctrine.** "Silence never means absent" is applied mechanically: empty
|
|
352
|
+
registry stdout, unparsed versions, non-translatable exit codes, unresolvable ownership,
|
|
353
|
+
corrupt-but-present rules inputs, all become exit 2 or a CRITICAL, never a pass.
|
|
354
|
+
- **No telemetry, no callbacks.** `border check` touches the network only for the
|
|
355
|
+
registry pre-flight, against URLs you configured (`npmjs.org`/`pypi.org` defaults, fully
|
|
356
|
+
overridable to private indexes).
|
|
357
|
+
- **No credential handling.** Subprocess publishes inherit stdio so tokens and OTP prompts
|
|
358
|
+
go between you and `npm`/`twine`, never through border.
|
|
359
|
+
- **State is inert.** Everything border writes lives in `.border/` (ledger, run archives,
|
|
360
|
+
dist, lock), which carries its own `.gitignore` with `*` so a `git add -A` cannot stage
|
|
361
|
+
it, and which every scan leg refuses to treat as a finding source or an allow-list
|
|
362
|
+
target. A repo that *tracks* `.border/**` is itself a CRITICAL finding. A single-writer
|
|
363
|
+
lock makes concurrent runs exit 2 instead of racing.
|
|
364
|
+
|
|
365
|
+
## License
|
|
366
|
+
|
|
367
|
+
MIT, see [LICENSE](LICENSE).
|
|
368
|
+
|
|
369
|
+
## 中文概要
|
|
370
|
+
|
|
371
|
+
border 是一个 fail-closed(失败即拦截)的推送前门禁 CLI:`npm install -g border-customs` 安装,在仓库里跑 `border check`。它扫描 git 历史、工作区(未跟踪文件同样是一等输入)、归档、tag 注释和将要发布的 npm/PyPI 字节,检出密钥与供应链风险;只有当"当前状态指纹"存在新鲜且完整的 PASS 记录时才允许 `border push --yes` 放行。指纹是 sha256(head、porcelain 摘要、规则哈希、暴露面、ref 集合、有效目标)六元组,任何一处变动,旧的 PASS 立即失效,必须重查。流水线:gitleaks(历史+工作区+tag,内置 8.30.1 规则,仓库自带的 ignore 文件直接判 CRITICAL)+ secretlint(进程内,AWS Key 规则强制开启)+ 原生规则(AI 会话产物闭集、提交身份白名单含传输对象检查)+ 注册表预检(版本已存在=必须 bump,名称被外人占有=拒绝;空响应/超时/解析失败一律 exit 2,沉默绝不等于不存在)。构件只构建一次进 `.border/dist/`,扫描的就是发布的字节,发布时再哈希比对,不一致直接拒发。跳过台账让重复检查不到 1 秒,但回放前先重算指纹并重新 pack 验证新鲜度。报告只输出掩码片段(sha256 摘要 + 前4…后4)。push 是多目标状态机,多 remote 先做全有或全无的 fast-forward 预检,永不 force-push;npm/twine 的凭据经 stdio 透传,border 从不触碰。可选 LLM 层 border 自身从不调用模型 API:`llm-request` 导出掩码审阅包,`llm-ingest` 严格校验 agent 结论并重算裁决。退出码即合同:0 通过、1 拦截、2 门禁无法作答,任何"工具不健康"都不可能被误读为干净。MIT 许可,无遥测,除你配置的注册表预检外不联网。
|