mirror of
https://github.com/runbear-io/beardrive.git
synced 2026-08-25 08:08:08 +02:00
* refactor(secrets): lift the share-time credential rules into internal/secrets The rules only ever ran on the rarest path a file takes. Moving them out of internal/webapp is what lets internal/syncer run the same six rules on the path every file takes, without inverting the dependency. Pure move plus one addition: Label(), the six human strings that until now lived only in the frontend's SECRET_LABELS — so 'bdrive share' stops printing a bare rule id where the web dialog says 'an AWS access key'. Rule ids and the rule/line JSON tags are unchanged: Browser.tsx keys off them, so they are a wire contract. * feat(sync): warn when a synced file looks like it holds a credential The six share-time rules now run on the path every file takes. A file with an AWS key in it used to ride a normal sync to the hub, to every teammate's disk and into every future agent's context with no badge and no warning — while the Share dialog one click later blocked that exact file. Warn, never block: the op is journaled and pushed exactly as before. A hold arm would mean a false positive silently parks someone's changes, and it would break the cycle's degrade-to-offline posture. - scan() reads the blob PutBlobFile just wrote (the bytes that were actually journaled), only on the branches that wrote one — an unchanged file is still never re-read. - Findings persist per path in secrets-<mount>.json, merged rather than replaced: nearly every cycle scans zero files, and a whole-set rewrite would erase the warning seconds after it appeared. Fixing the file clears it. - bdrive status grows a secrets block; the agent hook appends one advisory sentence. Rule ids and line numbers only, never the matched bytes. - SaveSecrets failing logs and continues: advisory telemetry never gets a veto over convergence. * docs: the credential check now runs on sync, not only on share README, the CLI reference and project-files get the new bdrive status block and the warn-never-block posture, with the three limits stated (checked when it changes, first 1 MiB, writing device only). Diagrams: internal/secrets is a package of its own in the overview, secretLog joins the sync engine, and the share-gate class notes that it no longer owns the rules. * test(sync): assert an unchanged file is never re-read for credentials The check must ride the branch that already reads the file. Clearing the record by hand and cycling proves it: a scan that re-read unchanged files would put the finding back, and the daemon's 3-second tick would pay for it on every file.
96 lines
2.9 KiB
Go
96 lines
2.9 KiB
Go
package syncer
|
|
|
|
import (
|
|
"io"
|
|
|
|
"github.com/runbear-io/beardrive/internal/secrets"
|
|
"github.com/runbear-io/beardrive/internal/store"
|
|
)
|
|
|
|
// The credential check runs on the path every file takes — the sync scan —
|
|
// and it only ever WARNS. The op is journaled and pushed exactly as it was
|
|
// before this check existed; a hit adds a line to `bdrive status` and a
|
|
// sentence to the agent hook's context, and nothing else. Holding the op back
|
|
// would mean a false positive silently parks someone's changes, which costs
|
|
// more trust than the check buys, and it would break the cycle's "degrade to
|
|
// offline, never fail" posture. Warn keeps that; hold breaks it.
|
|
//
|
|
// It reads the BLOB the scan just wrote, not the working file again: those are
|
|
// the exact bytes that were hashed and journaled, so a line number can never
|
|
// describe content no op ever captured, and the read is the same page-cache
|
|
// read the hash pass just did.
|
|
//
|
|
// Nothing here may fail a cycle. Every error path drops the finding.
|
|
|
|
// secretsPerFile caps what one file contributes. A generated file full of
|
|
// key-shaped strings would otherwise put thousands of lines into a status
|
|
// report nobody can read, and the first few are what a person acts on.
|
|
const secretsPerFile = 8
|
|
|
|
// secretLog is one cycle's view of the mount's findings: loaded whole, merged
|
|
// per path, written back only if the cycle actually touched it. Per-path merge
|
|
// is the point — nearly every cycle scans zero changed files, and a whole-set
|
|
// rewrite would erase the warning seconds after it appeared.
|
|
type secretLog struct {
|
|
found map[string][]secrets.Finding
|
|
dirty bool
|
|
}
|
|
|
|
// scanBlob records what the blob just written for rel contains. Called only on
|
|
// the branches that ran PutBlobFile, so an unchanged file is never re-read.
|
|
func (l *secretLog) scanBlob(st *store.Store, rel, sum string) {
|
|
if l == nil {
|
|
return
|
|
}
|
|
f, err := st.OpenBlob(sum)
|
|
if err != nil {
|
|
return // unreadable: skipped like every other unreadable-file case
|
|
}
|
|
defer f.Close()
|
|
buf, err := io.ReadAll(io.LimitReader(f, secrets.ScanLimit))
|
|
if err != nil {
|
|
return
|
|
}
|
|
found := secrets.Scan(buf)
|
|
if len(found) > secretsPerFile {
|
|
found = found[:secretsPerFile]
|
|
}
|
|
l.set(rel, found)
|
|
}
|
|
|
|
// set records rel's current findings, dropping the entry when there are none —
|
|
// which is how fixing the file clears the warning with no command and no flag.
|
|
func (l *secretLog) set(rel string, found []secrets.Finding) {
|
|
if len(found) == 0 {
|
|
l.drop(rel)
|
|
return
|
|
}
|
|
if !sameFindings(l.found[rel], found) {
|
|
l.found[rel] = found
|
|
l.dirty = true
|
|
}
|
|
}
|
|
|
|
// drop forgets a path: its credential is gone, or the path is.
|
|
func (l *secretLog) drop(rel string) {
|
|
if l == nil {
|
|
return
|
|
}
|
|
if _, ok := l.found[rel]; ok {
|
|
delete(l.found, rel)
|
|
l.dirty = true
|
|
}
|
|
}
|
|
|
|
func sameFindings(a, b []secrets.Finding) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
for i := range a {
|
|
if a[i] != b[i] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|