security watch: Docker Hub requests spaced, a 429 retried (NOT CHECKED only after the last try), tag lists cached 24 h per registry path on the runner (R-1334); W14
gates / gates (push) Successful in 4m14s
gates / gates (push) Successful in 4m14s
This commit is contained in:
1 parent
1c8f9ef924
commit
fe677bae94
4 files changed
+158
-13
No files matched your search
@@ -22,6 +22,8 @@ What is pinned, each by its consequence (the mail or the class), not by its mech
|
||||
the catalog's lane; a DooPlex image in a private registry is NOT CHECKED with the reason; its own weekly block
|
||||
W13 one image in the catalog AND on DooPlex (R-1333): a new notice is mailed for both pins; the memory keeps one
|
||||
entry per group; an old shared key is copied, so nothing is mailed twice; a clean DooPlex pin forgets only its own
|
||||
W14 Docker Hub (R-1334): a 429 is retried (Retry-After honoured) and is NOT CHECKED only after the last try; a tag list
|
||||
cached under 24 h is not fetched, an older one is; two names of one path fetch once; requests are spaced
|
||||
W11 ghcr.io tag lists are PAGED (Link: rel="next"): every page is read, so a newer tag on page 2 is seen
|
||||
Red-proofs (2026-10-26): W5 with the `k not in known` test removed (every night mails) -> W5 fails; W6 with the streak
|
||||
threshold at 1 -> W6 fails; W2 with FEED_DAYS check removed -> W2's old-item case fails; W4 with SourceError swallowed
|
||||
@@ -393,7 +395,7 @@ def t_runtime():
|
||||
src = open(os.path.join(HERE, "security_watch.py"), encoding="utf-8").read()
|
||||
check("W10 main() hands the reader to run() (off only with --no-runtime)",
|
||||
"rreader = None if \"no-runtime\" in opts else cached_runtime_reader(rcache)" in src
|
||||
and "runtime=rreader)" in src)
|
||||
and "runtime=rreader, tag_cache=tcache)" in src)
|
||||
check("W10 the reader counts what it read and from the cache (the log's RUNTIME line)",
|
||||
r.stats["read"] == 2 and r.stats["cached"] == 1 and r.stats["not_checked"] == 1 and "RUNTIME read=" in src,
|
||||
r.stats)
|
||||
@@ -470,11 +472,76 @@ def t_two_groups():
|
||||
["rommapp/romm|CVE-2026-111497"], sorted(st4["advisories"]))
|
||||
|
||||
|
||||
class SeqNet(FakeNet):
|
||||
"""FakeNet whose route may be a LIST of answers, served in order (the last one repeats)."""
|
||||
def get(self, url, headers=None, timeout=30):
|
||||
for pfx, ans in self.routes.items():
|
||||
if url.startswith(pfx) and isinstance(ans, list):
|
||||
self.seen.append(url)
|
||||
st, h, body = ans.pop(0) if len(ans) > 1 else ans[0]
|
||||
return st, h, body if isinstance(body, bytes) else json.dumps(body).encode()
|
||||
return FakeNet.get(self, url, headers, timeout)
|
||||
|
||||
|
||||
def t_dockerhub():
|
||||
print("-- W14: Docker Hub — retry a 429, cache a tag list for 24 h, one fetch per path, paced (R-1334)")
|
||||
hub = "https://hub.docker.com/v2/repositories/library/redis/tags"
|
||||
ok = (200, {}, {"results": [{"name": "7.4.1"}, {"name": "7.4.0"}], "next": None})
|
||||
r429 = (429, {"Retry-After": "7"}, b"")
|
||||
slept = []
|
||||
clock = [1000.0]
|
||||
|
||||
def sleep(s):
|
||||
slept.append(s)
|
||||
clock[0] += s
|
||||
|
||||
def mk(net, cache=None, now=1000.0):
|
||||
return w.Sources(net, TODAY, False, tag_cache=cache, now=now, sleep=sleep, clock=lambda: clock[0])
|
||||
net = SeqNet({hub: [r429, ok]})
|
||||
names = mk(net).tags("redis")
|
||||
check("W14 a 429 is retried after its Retry-After, and the retry's tags are used",
|
||||
names == ["7.4.1", "7.4.0"] and len(net.seen) == 2 and 7 in slept, (names, net.seen, slept))
|
||||
net = SeqNet({hub: [r429]})
|
||||
try:
|
||||
mk(net).tags("redis")
|
||||
res = "no error"
|
||||
except w.SourceError as e:
|
||||
res = str(e)
|
||||
check("W14 a 429 on every try -> NOT CHECKED only after the last try (%d tries)" % w.HUB_TRIES,
|
||||
"429" in res and len(net.seen) == w.HUB_TRIES, (res, len(net.seen)))
|
||||
cache = {"docker.io/library/redis": {"at": 1000.0 - 3600, "tags": ["7.4.1"]}}
|
||||
net = SeqNet({hub: [ok]})
|
||||
names = mk(net, cache).tags("redis")
|
||||
check("W14 a tag list cached an hour ago -> no request", names == ["7.4.1"] and net.seen == [], (names, net.seen))
|
||||
cache = {"docker.io/library/redis": {"at": 1000.0 - 25 * 3600, "tags": ["7.4.0"]}}
|
||||
net = SeqNet({hub: [ok]})
|
||||
names = mk(net, cache).tags("redis")
|
||||
check("W14 a tag list cached 25 h ago -> fetched again, and the cache holds the new list",
|
||||
names == ["7.4.1", "7.4.0"] and len(net.seen) == 1 and cache["docker.io/library/redis"]["at"] == 1000.0,
|
||||
(names, net.seen, cache))
|
||||
lsio = "https://hub.docker.com/v2/repositories/linuxserver/sonarr/tags"
|
||||
net = SeqNet({lsio: [(200, {}, {"results": [{"name": "4.0.20"}], "next": None})]})
|
||||
src = mk(net)
|
||||
a, b = src.tags("lscr.io/linuxserver/sonarr"), src.tags("linuxserver/sonarr")
|
||||
check("W14 two names of one Docker Hub path -> one fetch", a == b == ["4.0.20"] and len(net.seen) == 1, net.seen)
|
||||
slept.clear()
|
||||
net = SeqNet({hub: [ok], lsio: [(200, {}, {"results": [{"name": "4.0.20"}], "next": None})]})
|
||||
src = mk(net)
|
||||
src.tags("redis")
|
||||
src.tags("linuxserver/sonarr")
|
||||
check("W14 Docker Hub requests are spaced (>= %.1f s apart)" % w.HUB_GAP_S,
|
||||
slept and abs(sum(slept) - w.HUB_GAP_S) < 0.01, slept)
|
||||
src_text = open(os.path.join(HERE, "security_watch.py"), encoding="utf-8").read()
|
||||
check("W14 main() loads the runner's tag cache, hands it to run() and prints the TAGS line",
|
||||
"tag_cache=tcache)" in src_text and "felhom-watch-tags.json" in src_text and "TAGS fetched=" in src_text)
|
||||
|
||||
|
||||
def main():
|
||||
t_ranges()
|
||||
t_classes()
|
||||
t_memory()
|
||||
t_two_groups()
|
||||
t_dockerhub()
|
||||
t_inventory()
|
||||
for t in (t_runtime, t_ghcr_pages, t_dooplex):
|
||||
try:
|
||||
|
||||
Reference in new issue
Block a user