carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1package objstore
2
3import "context"
4
5// Scanner is the scanning-engine seam (spec §3): everything the sweep needs an
6// engine to do, and nothing an app ever calls. Like Provisioner, every method
7// is idempotent — "already enabled" and "already gone" are successes — because
8// the sweep that drives one must be re-runnable after a half-failure.
9//
10// v1 ships the ClamAV implementation and this package's in-memory fake. A
11// GuardDuty scanner arrives here later without touching the sweep, once the
12// question in spec §4.2 has been measured: AWS tags only objects uploaded
13// after tagging was enabled, and every object in an existing store predates
14// its plan, so if an on-demand scan does not tag them then that engine needs a
15// different verdict source and not just a different implementation of this.
16type Scanner interface {
17 // EnsureEnabled turns scanning on for a bucket where the backend scans
18 // natively. Engines that scan by fetching have nothing to configure.
19 EnsureEnabled(ctx context.Context, bucket string) error
20 // EnsureDisabled releases whatever EnsureEnabled took. Teardown calls it
21 // BEFORE deleting the bucket (spec §7.3): the record is the only thing
22 // naming the backend estate, so releasing it after the record is gone
23 // leaks an estate nobody can find again.
24 EnsureDisabled(ctx context.Context, bucket string) error
25 // Verdict answers the recorded verdict for one object AT etag. A verdict
26 // recorded against a DIFFERENT etag is (VerdictUnknown, nil): the object
27 // has been rewritten since it was judged, and bytes nobody looked at have
28 // no verdict. Absent is (VerdictUnknown, nil) too — the same idiom Get
29 // uses for an absent record, because "not scanned" is a state.
30 Verdict(ctx context.Context, bucket, key, version string) (Verdict, error)
31 // Status answers what is known about the CURRENT version of one object,
32 // and returns the object as a listing page would have described it so the
33 // caller can go on to Scan it without a HeadObject of its own.
34 //
35 // TWO DIFFERENCES FROM Verdict, and both matter.
36 //
37 // It finds the current version itself, because a caller holding only a key
38 // — the on-demand drain, the status door — has no listing page to take one
39 // from. Handing them the token to reconstruct would leak an internal
40 // format and hand them the laundering bug as a correctness bug.
41 //
42 // And it does NOT apply Verdict's re-issue rule. A stored `denied` or
43 // `failed` comes back as itself here. That rule is SWEEP RETRY POLICY —
44 // re-scan the transient ones — and it is the wrong answer to "what
45 // happened to this object": left in place, a failed scan would read as
46 // pending forever, because the drain deletes the item as scanned and
47 // nothing would ever change the answer.
48 //
49 // ErrObjectGone when the object is not there. A denied HeadObject is
50 // (VerdictDenied, ScanObject{}, nil) — there is no version or size to
51 // scan with, so a caller must not go on to Scan a zero ScanObject.
52 Status(ctx context.Context, bucket, key string) (Verdict, ScanObject, error)
53 // Scan scans one object and records its verdict against etag. It returns
54 // the verdict when the engine is synchronous (ClamAV) and VerdictUnknown
55 // when the engine scans asynchronously (GuardDuty). The sweep counts what
56 // it gets back, so a synchronous engine's FIRST pass is a pass full of
57 // real verdicts rather than one full of "unscanned".
58 //
59 // size comes from the listing page the sweep is already holding, so an
60 // engine can refuse an over-ceiling object without fetching it. Paying
61 // egress for bytes the scanner will reject is pure waste, and the
62 // alternative — a HeadObject per object per sweep — is a round trip on
63 // every object forever.
64 // recorded reports whether the verdict was actually PERSISTED to the
65 // object. It is not always: tag() declines to write when the object
66 // already carries eight foreign tags (S3 caps an object at ten) or when
67 // the pre-write tag read is denied. Both are deliberate — they stop one
68 // object stalling a whole sweep — and the sweep tolerates them because it
69 // re-lists every 24h.
70 //
71 // A queue item does not. Without this flag the on-demand drain would
72 // report clean, delete the item, and the status door would answer pending
73 // forever, because the tag is not there — the app's declared source of
74 // truth never arriving while a callback said otherwise.
75 Scan(ctx context.Context, bucket, key, version string, size int64) (verdict Verdict, recorded bool, err error)
76 // Enforcement names the object tag this engine writes for an infected
77 // object, so the IAM policy writer can deny reads on it (spec §8).
78 // Engine-specific by necessity: a Deny statement must name a literal key
79 // and value, which is the one place the sweep's engine-blindness cannot
80 // hold.
81 Enforcement() DenyTag
82 // Capacity reports how many buckets this engine can still protect and the
83 // cap; (-1, -1) is uncapped. ClamAV is uncapped. GuardDuty caps at 25 per
84 // account per Region, not adjustable, against a platform that mints one
85 // bucket per app — which is why it is not the default engine.
86 Capacity(ctx context.Context) (remaining, cap int, err error)
87 // Provenance names what did the scanning — the engine and its signature
88 // database version — for the evidence record. "Scanned with what?" is an
89 // audit question, and a clean verdict from three-month-old signatures
90 // should not read like a fresh one.
91 Provenance(ctx context.Context) (engine, version string, err error)
92}
93
94// Verdict is one object's scan result. The zero value is "no recorded
95// verdict", which is a state and not an error.
96type Verdict string
97
98const (
99 VerdictUnknown Verdict = ""
100 VerdictClean Verdict = "clean"
101 // VerdictInfected is the only verdict the Deny statement acts on.
102 VerdictInfected Verdict = "infected"
103 // VerdictSkipped is too large, unsupported, or unextractable. It is NOT
104 // clean, and the evidence must never blur the two: a store that is mostly
105 // skips has not been meaningfully scanned.
106 VerdictSkipped Verdict = "skipped"
107 // VerdictDenied is the engine being unable to read the object. Distinct
108 // from Failed because the repair is a permissions fix, not a retry.
109 VerdictDenied Verdict = "denied"
110 // VerdictFailed is the engine reading the object and failing on it. It is
111 // NOT "the engine was unreachable" — an outage must never stamp thousands
112 // of objects with a verdict about their contents.
113 VerdictFailed Verdict = "failed"
114)
115
116// DenyTag is the object tag an engine writes for an infected object, and the
117// literal a Deny statement matches on (spec §8).
118//
119// It lives in the RECORD rather than being asked of a Scanner at policy-write
120// time, because Grant takes a Provisioner and has no Scanner to ask. That is
121// also what lets `scan enforce off` clear a deny on a deployment with no
122// engine built — without it, unsetting CARLOS_STORE_SCANNER on a deployment
123// holding any enforcing record would leave that store unrepairable.
124type DenyTag struct {
125 Key string `json:"key"`
126 Value string `json:"value"`
127}
128
129// The tags the ClamAV engine writes. TagVersion is what binds a verdict to the
130// bytes it judged: without it a tag says only "some bytes under this key were
131// clean once", and a re-uploaded object would inherit its predecessor's
132// verdict.
133//
134// The version is the ETag AND the last-modified time (ScanObject.Version), not
135// the ETag alone. A single-part upload's ETag is the content MD5, and
136// chosen-prefix MD5 collisions are practical on ordinary hardware — an
137// attacker controlling both uploads could otherwise give a payload the ETag of
138// the clean object it replaces. Last-modified moves on every overwrite and is
139// not theirs to choose.
140const (
141 TagVerdict = "carlos-scan"
142 TagVersion = "carlos-scan-version"
143 TagAt = "carlos-scan-at"
144)
145