Spaces:
Sleeping
Sleeping
Commit History
-
test(webhooks): add regression test for the redirect-based SSRF bypass
New test spins up two real local HTTP servers (one redirecting, one "internal") and asserts the internal one is never reached. Confirmed failing against the pre-fix code first (the internal server's handler genuinely fires), then passing against the fix. Also updates one existing test that mocked the old module-level urlopen name directly -- now mocks _NO_REDIRECT_OPENER.open, the actual new call site. Not a behavior change, just following the implementation detail the previous commit changed.36c7f8b -
fix(webhooks): stop following redirects on webhook delivery (SSRF bypass)
urlopen()'s default opener follows HTTP redirects automatically. The SSRF guard validates the registered URL's hostname (at registration and again at delivery, per F-9) but never inspects a redirect's Location header -- a webhook target that legitimately passes validation can redirect to an internal address (e.g. the cloud metadata IP) at delivery time and have it followed transparently. Verified empirically against a real local HTTP server before writing this fix. Now uses a custom opener that refuses to follow redirects, surfacing them as an ordinary delivery failure through the existing retry/suspend/logging path instead. Found independently, not from the numbered audit findings (I don't have that document's detailed sections).1a5e945
-
docs(config): clarify which startup path loads which .env.example
backend/core/config.py's env_file=".env" is cwd-relative, and README.md / SETUP_GUIDE.md document two different working directories to start the server from -- so these three files are each load-bearing for a different real path, not accidental duplicates. Added a header to each explaining which path loads it, instead of deleting any of them. Verified directly against config.py, both docs, and the Dockerfile. No settings changed, no code changes.fcd6b46
-
fix(deps): bump transformers to 5.10.1 (CVE-2026-9856)
7820cb8 -
test(analyze): add regression tests for the C-3 full-fix pipeline
21 tests covering _prepare_image_bytes() directly, each migrated endpoint's new quality-gate/validation rejection, the /platform+/c2pa non-rotation guarantee (with an /attribution contrast case proving the difference is deliberate config, not a test artifact), /segment's new content-type check, and /batch's skip-and-continue behavior. Verified: fails in the predicted ways against the pre-fix code (18/21 failing -- ImportError, 200 instead of 422/415, unrotated instead of rotated, 3 processed instead of 2 -- see this PR's description for the full revert-and-confirm proof), passes cleanly against the fix.31a0502 -
fix(analyze): migrate /export/{fmt}, /image/stream
Same shared pipeline as the rest. /image/stream previously streamed spatial signals (PRNU, CFA, noise map, etc.) on a rotated iPhone photo without ever correcting its orientation -- now consistent with every other analysis endpoint. /export/{fmt}'s cache key is now computed on the post-EXIF-correction bytes, matching /image (previously hashed the raw upload -- no test depends on the literal hash value). This is the last of the 9 endpoints -- all file-accepting routes in this router now share one validation pipeline. Verified: full suite still green, fresh-checkout diff clean.eaa6846 -
fix(analyze): migrate /segment, add its missing content-type check
/segment was the only one of this router's 9 file-accepting endpoints with no content-type header check at all -- real image bytes with a mismatched declared content-type were previously accepted (the C-3 stopgap's validate_file() sniffs actual content, not the header). It now 415s consistently with every sibling endpoint, and also gains EXIF correction and the quality gate via the same shared pipeline. No existing test relied on the missing check (confirmed: zero endpoint-level tests existed for /segment before this PR); new coverage is included in Commit 7. Verified: full suite still green, fresh-checkout diff clean.348cbba -
fix(analyze): migrate /batch onto _prepare_image_bytes() per file
Each file in a batch now gets EXIF correction, MIME/extension validation, and the quality gate -- none of which /batch had before, beyond a bare per-file size check. Uses _prepare_image_bytes() directly (not prepare_upload()) since /batch already has its own whole-request Content-Length precheck before this loop; wrapped in the same skip-invalid-and-continue pattern already used for content-type and size. Verified: full suite still green, fresh-checkout diff clean.c89e868 -
fix(analyze): migrate /platform, /c2pa with correct_exif=False
Same shared pipeline, but EXIF correction deliberately disabled: both endpoints fingerprint properties of the ORIGINAL file structure (EXIF presence/absence for /platform; a binary JUMBF C2PA manifest for /c2pa) that a re-encode would corrupt or destroy. Still gain MIME/extension validation and the quality gate. Verified: full suite still green, fresh-checkout diff clean, plus new regression tests proving neither endpoint rotates an oriented image (test_c3_full_fix_prepare_upload.py, added in Commit 7).c559bab -
fix(analyze): migrate /image/heatmap, /attribution, /robustness
Same shared prepare_upload() pipeline as /image (Commit 1). All three previously had content-type + size checks only -- now also get EXIF correction, MIME/extension validation, and the quality gate. Verified: full suite still green, fresh-checkout diff clean.600f3fc -
fix(analyze): add shared prepare_upload() pipeline, migrate /image
Extract /image's six upload guards (content-type check, Content-Length precheck, EXIF correction, post-read size check, MIME/extension validation, quality gate) into _prepare_image_bytes()/prepare_upload(), shared functions the other 8 file-accepting endpoints migrate onto in the following commits. /image now calls prepare_upload() instead of its own inline copy of this logic; removed the now-dead except FileValidationError clause this makes unreachable. Verified: full suite still green, fresh-checkout diff clean.f5b299a
-
refactor: retire 3 dead legacy detector facades, rewrite their tests
AdvancedAIDetector/UltraAdvancedDetector/CovarianceDetector had zero production imports anywhere -- confirmed by repo-wide search -- kept alive only by their own dedicated test files constructing them directly. Audited every assertion in those 3 test files (18 tests) against the rest of the suite before deleting anything: - "complete detection" tests (exactly 10/13/16 signals) tested a scoped subset that doesn't exist anywhere reachable anymore -- StatisticalDetector always runs the full 19. Not preserved. - "forensics_integration" tests (total_signals == 30) were near- identical duplicates across all 3 files, already covered repeatedly by test_determinism.py/test_invariants.py/test_api_stress.py. - Every remaining assertion (8 of 18) tested something not covered anywhere else -- specific signal_name/method/confidence values for DCT Coefficients, Wavelet Energy, RGB Noise Covariance, Patch Spectral Variance, Natural Prior Deviation, Eigenvalue Spread, Local Covariance …23de5d3
-
docs(report-exporter): cross-reference the frontend's separate PDF path
Matching note on the backend side: this module is reached only via a from-scratch re-analysis, never the already-computed report the frontend's download button uses. Same rationale as the frontend commit.a4a1dd2
-
refactor(sse): use shared json_safe.sanitize()
Removes the local, incomplete duplicate (was missing numpy-scalar conversion, unlike analyze.py's copy) in favor of the shared implementation both now use.406c86b -
refactor(analyze): use shared json_safe.sanitize()
Replaces the module-level _sanitize() definition with an import from the new shared module. All existing call sites unchanged (imported under the same local name).091223d -
refactor(utils): add shared json_safe.sanitize()
Extracted from analyze.py's _sanitize(), which sse_analyzer.py had a second, near-duplicate (and already-drifted) copy of. Also fixes a real bug found while writing tests for this: a numpy NaN/Infinity scalar was converted to a native float but never actually checked for being non-finite, because the numpy-conversion branch returned early. Both duplicates had this bug; fixed here rather than carried forward.91db04e
-
fix(deps): bump regex for transformers 5.5.0 compatibility
pip's resolver: transformers 5.5.0 needs regex>=2025.10.22; this project pinned 2023.12.25. clip's own regex dependency is unversioned, so it doesn't conflict with the bump.4ae0618
-
fix(deps): bump transformers to 5.5.0, closing 5 CVEs
pip-audit found PYSEC-2025-217/PYSEC-2026-2288/2289/2290 in transformers==4.57.6; 5.5.0 satisfies all listed fix versions. transformers v5 requires huggingface_hub>=1.0.0,<2.0 -- bumped that too (0.34.0 -> 1.4.0). No other pinned dependency needed a change: torch==2.7.1, accelerate==1.7.0, and Pillow==12.3.0 already satisfy v5's stated minimums. Not directly imported anywhere in backend/ -- pulled in transitively by diffusers for CLIP text encoding in the SD pipeline DIRE uses. diffusers==0.38.0 is confirmed compatible with transformers 5.x in production elsewhere; the one documented diffusers+v5 break (missing MT5Tokenizer) only affects HunyuanDiT pipelines, which this project doesn't use. Could not test this end-to-end here (no functional native torch in this sandbox). This project's own CI now actually runs the full suite against these exact pinned versions (see H-6) -- if it turns up a real incompatibility, revert just this commit; the cryptography and setuptools fixes above sta…6e4555d -
fix(deps): pin setuptools to close PYSEC-2026-3447
Was unpinned (resolved transitively to 79.0.1). Added an explicit floor at 83.0.0, the fix version pip-audit reported. Build-time only (MANIFEST.in exclusion bypass on macOS); no app-code impact.2aa5985 -
fix(deps): bump cryptography to close 3 CVEs
pip-audit (now enforced, see H-6) found PYSEC-2026-3552/3553/3554 in cryptography==48.0.1. Bumped to 50.0.0, satisfying all three fix versions. Not directly imported by app code (pinned only to close a transitive CVE, like idna/Pygments above); low risk.157e26c
-
test(fuzz): stop hiding signal crashes behind pytest.skip()
test_random_valid_images() caught (TypeError, ValueError) and skipped instead of failing, masking the exact crash class fixed in the previous commit. Now asserts the detector never raises. Added test_h3_signal_isolation.py covering the fix directly.b70fbbd -
fix(signals): isolate exceptions in statistical signal bundle
16 of 19 signals in BasicSignals/UltraSignals/CovarianceSignals had no exception handling; one failure crashed the whole 12-signal ensemble. Added _safe_compute() wrapper, applied across all three compute_all(). Verified: a 1x1 image reproducibly crashes the original code (ValueError in analyze_fft_radial_spectrum); fixed code returns a neutral result instead. New tests confirm isolation, not just survival. Full suite: no regressions.8bc399c -
fix(analyze): add missing per-file and total size checks to /batch
Problem: /batch had NO size check anywhere in the route handler -- the 5MB MAX_IMAGE_BYTES cap only existed inside batch_processor.process_batch(), checked only after all (up to 10) files were already fully buffered into memory in the route handler's own loop. Fix: added a coarse pre-read guard on the whole request's declared size (MAX_BATCH_SIZE * MAX_IMAGE_BYTES), plus a genuine per-file check right after each file's own read, skipping an individually oversized file rather than retaining it for the downstream pipeline -- matching the existing pattern of skipping unsupported content-types in the same loop. Verified: added the full regression suite for this delivery (test_c3_content_length_precheck.py, 15 tests) covering all 9 endpoints touched across commits 1-3. Each previously-unguarded endpoint is called directly with an oversized Content-Length header and a mock UploadFile whose .read() raises if called, proving the guard fires strictly before any read. Directly proved the origi…f00d03b -
fix(analyze): correct /segment's size ceiling and add missing file.close()
Problem: /segment relied solely on validate_file()'s general-purpose 50MB (MAX_FILE_SIZE_MB) ceiling -- five times higher than the 10MB (MAX_ANALYSIS_SIZE_MB) limit every sibling CPU-heavy analysis endpoint enforces. It was also the only file-accepting endpoint in this router with no finally: file.close(). Fix: added the same pre-read Content-Length guard and an explicit post-read check, both using MAX_ANALYSIS_SIZE_BYTES; added finally: await file.close(); added except HTTPException: raise as the first exception clause (closes a pre-existing latent NameError risk, where an exception raised before FileValidationError is imported inside the same try block could otherwise surface as a confusing NameError instead of the intended error). Verified: regression test confirms a file just over 10MB (well under the old 50MB ceiling) is now rejected at the correct, intended ceiling.0b8ca8f -
fix(analyze): add pre-read Content-Length guard to 7 endpoints missing it
Problem: /image/heatmap, /attribution, /platform, /c2pa, /robustness, /export/{fmt}, and /image/stream all read the entire request body via await file.read() before checking its size at all -- meaning an oversized upload was fully buffered into memory before ever being rejected. Only POST /image had this pre-check. Fix: added a shared _reject_if_content_length_exceeds(request, max_bytes) helper (mirroring /image's existing check) and wired it into all 7 endpoints, before their respective await file.read() call. Verified: proved the gap directly against the original code by calling the real, unpatched endpoint function with an oversized Content-Length header and a mock UploadFile whose .read() raises if called -- read() was reached, confirming the missing guard. Added regression tests (test_c3_content_length_precheck.py) using the same technique to prove the fix: each endpoint now raises HTTP 413 WITHOUT ever calling file.read(). Full existing suite shows zero regressions (same 7 pre-…ee4c4cb -
fix(analyze): stop silently recompressing every upload during EXIF orientation correction
Problem: the EXIF-orientation-correction step in POST /api/v1/analyze/image unconditionally re-encoded EVERY uploaded JPEG -- even when no rotation was needed -- with no quality= argument, silently defaulting to Pillow's JPEG quality=75 regardless of the original quality. Measured on a real quality-95 synthetic photo: ~55% smaller, mean pixel error 12.65/255. This happens before any forensic signal runs, corrupting the input to the ~31% of ensemble weight (ELA, JPEG Ghost, DCT, PRNU-heuristic, Noise Map, Noiseprint, CFA) that specifically measures compression artifacts, for most real phone-camera uploads (virtually all of which carry an orientation tag, even the identity value 1 -- ImageOps.exif_transpose() returns a fresh copy in that case too, confirmed by direct testing, so checking "did exif_transpose change anything" cannot be used as the guard). Fix: (1) only re-encode when the Orientation tag is present and not the identity value (1) -- skip re-encoding entirely otherwise, so t…5d0356c -
fix(forensics): resolve GPS EXIF sub-IFD correctly to stop crashing on geotagged photos
Problem: extract_exif() crashed with "TypeError: 'int' object is not iterable" on any photo carrying GPS EXIF data (the majority of unedited smartphone photos) -- confirmed by direct reproduction against a real geotagged JPEG, not inferred from reading the code. Root cause: PIL.Image.getexif() returns the GPSInfo tag (0x8825) as an unresolved IFD pointer -- a plain int (the sub-IFD's byte offset in the file) -- not a dict of GPS sub-tags. The previous code assumed it was already a dict and iterated it directly (`for gps_tag_id in value`), which raises immediately on any image with a GPS tag present. This is a different failure mechanism than a prior third-party audit assumed (it described the crash happening later, at JSON-serialization time, due to an un-converted IFDRational value) -- that description doesn't match the actual code path, which crashes earlier and via a different exception type. The practical consequence (a 500 on any geotagged photo, discarding the full computed anal…c3d7d20
-
test(dire): regression test for scheduler isolation + doc update
test_scheduler_is_not_shared_across_instances: two DIREDetector instances that both hit the cache must get two distinct scheduler objects, not references to the same one. Verified as a real regression test, not just plausible -- reverted the fix and confirmed this test fails with exactly the shared-object assertion error, restored it and confirmed it passes. Updates PROFILING_F17.md's scheduler-sharing section to reflect the fix -- marked resolved, with a note that this removes the thread-safety blocker on ever parallelizing DIRE/CLIP/own-embedding but doesn't by itself answer whether it's worth doing (still needs a real DIRE timing number, unchanged from before this fix).cfffa10 -
fix(dire): clone scheduler instead of sharing across instances
DDIMScheduler.set_timesteps()/step()/add_noise() mutate instance state -- every DIREDetector instance previously took cached_model['scheduler'] directly (the process-wide ModelCache, F-7), meaning every request (a fresh DIREDetector each time) shared the exact same scheduler object. Two concurrent DIRE calls racing on set_timesteps()/step() against that one shared object was a real thread-safety risk, flagged during F-17 profiling as the reason DIRE wasn't included in that round's parallel signal pool. Adds _clone_scheduler(), called on both cache-hit paths: type(cached_scheduler).from_config(cached_scheduler.config) -- no disk/network I/O, just object construction from a config dict, so negligible overhead per request. self.pipe (the actual UNet/VAE weights) stays shared -- frozen, eval-mode forward passes don't mutate shared state, so that part remains safe to share. Also fixes test_load_model_is_not_duplicated_under_concurrency's _FakeScheduler mock, which broke under this change …530738b
-
test(ci): regression tests for the ModelCache pre-warm hook
Two tests: the hook must touch all three detector classes (dire, clip, own), not just one or two -- any of them could be the one an unlucky test hits first if it were missed. The hook must never raise even when the underlying load fails outright (simulated via a constructor that raises) -- pre-warming is an optimization, not a correctness requirement, and a failure here must not fail the whole session. Run for real in the delivering session's sandbox (torch stubbed, so the actual pre-warm attempt genuinely fails every time there) -- both pass, and the full suite re-run afterward shows the same 539 passed / 7 failed split (537 + these 2 new tests), matching the pre-existing baseline failure set exactly.fd3309b -
fix(ci): pre-warm ModelCache in pytest_sessionstart
test_advanced_ai_detector.py::test_forensics_integration timed out in CI at 60s (PR #166). Not caused by that PR's actual changes -- root cause: DIRE/CLIP/own-embedding load into a process-wide singleton cache, and the first test in the whole session to touch any of them pays the full cold-load cost (DIRE alone is a ~4-5GB download, no caching between CI runs). 12 test files trigger this indirectly through the full pipeline and none were marked @pytest.mark.slow -- only the files that directly unit-test DIRE/CLIP were. Whichever of those 12 happens to run first (pytest collects roughly alphabetically) absorbs the cost inside the fast tier's 60s per-test budget, at random depending on CI network conditions. Fixes the root cause instead of marking one more test slow: pytest_sessionstart runs once before collection/execution begins and isn't subject to any per-test timeout, so pre-warming there removes the timing risk for all 12 files at once. Best-effort/silent-fail -- if pre-warming ca…7716480 -
test(forensics): regression tests for the F-28 keyword expansion
16 tests: 7 parametrized editing-tool cases (4 previously-missed, 2 previously-working, 1 non-editor-software negative), 7 parametrized AI-marker cases (4 previously-missed, 2 previously-working, 1 ordinary-EXIF negative), a dedicated no-false-positive test on a mundane camera EXIF block, and a direct list-size regression check so an accidental revert to the old 5/4-entry lists is caught immediately rather than silently. Run for real in the delivering session's sandbox via a torch-stub technique (stub installed before pytest.main() starts, not per-test) -- all 16 pass. Also re-ran the full suite as a regression check: 537 passed, 7 failed out of 544, and the same 7 failures were confirmed present on the untouched pre-session baseline with identical error messages (missing clip/diffusers packages in this sandbox's stubbed torch, not caused by this or any other change this session).bc5a82a -
fix(forensics): F-28 — expand hardcoded editing-tool/AI-marker lists
detect_tampering_indicators() had two narrow hardcoded lists: editing_tools (5 entries -- photoshop/gimp/paint.net/pixlr/canva) and ai_keywords (4 entries -- midjourney/dall-e/stable diffusion/"ai generated"). Both missed most mainstream tools -- Lightroom, Affinity, Capture One, Topaz on the editing side; Adobe Firefly, Leonardo.Ai, Ideogram, Flux, Runway, and most other current generators on the AI-marker side. Pulled both out to module-level constants (EDITING_TOOLS, AI_GENERATION_MARKERS) and expanded to 25 and 35 entries respectively. Not exhaustive -- new tools/generators launch constantly -- but covers the mainstream ones as of this fix instead of a handful. Verified directly against 10 real EXIF-shaped cases: 6 previously- missed tools/generators now correctly flagged, 3 previously-working ones (Photoshop, GIMP, Midjourney) still flagged (no regression), 1 ordinary-camera-EXIF case confirmed zero false positives.a67565a
-
test(f17): regression tests for the concurrent signal pool
Covers what a passing test suite alone won't catch for a concurrency change: determinism across repeated runs (no race corrupting ai_probability), and the max_workers capping logic against cpu_count()=1 (must be 1, not 9), cpu_count()=64 (must be capped at 9, the actual task count, not 64), and cpu_count()=None (must be treated as 1, not passed through -- ThreadPoolExecutor interprets None as "pick a default from cpu_count()", which would silently reintroduce the oversubscription bug this whole change exists to prevent). Not run against the full app in the delivering session's sandbox (no working torch there -- see PROFILING_F17.md for the direct-function verification done instead, including these exact three cpu_count scenarios exercised against the real code). Run for real in this repo's own environment to confirm.0029235 -
perf(f17): parallelize the 9 independent non-torch signals
The 19-signal statistical bundle plus the 8 classical forensic detectors (prnu, ela, metadata, dct, jpeg_ghost, noise_map, noiseprint, cfa) were run one after another -- profiled sequential sum on a 1600x1200 JPEG: ~5.4s. They have no shared mutable state (every module was grepped for module-level caches/singletons first), so they now run in a ThreadPoolExecutor instead -- wall-clock drops to roughly the slowest single signal (~2.1s). DIRE/CLIP/own-embedding stay sequential, deliberately: DIRE's DDIMScheduler is cached and shared process-wide, and running it concurrently with itself needs that resolved first (see PROFILING_F17.md). max_workers is capped to os.cpu_count() (min'd against the 9 actual tasks, None treated as 1) rather than always spinning up 9 threads. This isn't defensive-for-its-own-sake: profiling this exact change on a single-core sandbox found 9 threads on 1 core measuring ~1.5x SLOWER than plain sequential, pure context-switch overhead with zero real parallelism. Th…8f1635f
-
Merge pull request #164 from abinaze/fix/webhook-ssrf-and-key-rotation
Unverified signatureFix/webhook ssrf and key rotation0adbe06
-
test(auth): regression tests for the F-5 analyze.py extension
Mirrors the existing test_viewer_role_can_reach_read_only_case_endpoints / test_viewer_role_cannot_create_cases pattern (cases.py) against analyze.py: a viewer key must reach GET /analyze/history (200) but get 403 on POST /analyze/image. Adds a third test confirming the F-1 public demo bypass still works unbroken by the new ROLES check. Not run against the full app/TestClient in the delivering session's sandbox (no working torch install there — see branch summary at the top of this file for the direct-function verification that was done instead). Structurally identical to the already-passing cases.py F-5 tests; run for real in this repo's own environment to confirm.52628b6 -
fix(auth): F-5 extension — enforce per-method ROLES on analyze.py
analyze.py used require_analyst_or_demo, a fixed-role check (any analyst/admin key, any HTTP method) — unlike cases.py, which the original F-5 fix already moved to require_role_for_method. That left analyze.py with the exact gap F-5 was supposed to close: a viewer-role key got 403 on every analyze.py endpoint, including plain GETs like /analyze/history and /analyze/stats, because ROLES["viewer"] = {"GET"} was never consulted for this router. Adds require_role_for_method_or_demo to backend/core/auth.py: the same ROLES[entry["role"]] vs request.method check require_role_for_method does, with require_analyst_or_demo's F-1 demo-token bypass checked first (fail-closed, same rule — only accepted when settings.PUBLIC_DEMO_KEY is non-empty; a match is treated as role="analyst"). Wires it into analyze.py's router in place of require_analyst_or_demo. Verified directly against the patched function (no app import needed — auth.py has no torch dependency): 12 cases covering no-auth, invalid key, …19ca01f
-
test(F-16h): stronger SSE regression test that would have caught F-16g
The existing tests in this file use loose assertions -- an OR across event types ('started' OR 'signal' OR 'summary' present), and a soft break-on-first-match with no final check that anything was actually found -- that let the real regression in the previous commit pass completely undetected through the full suite. Adds a test asserting the stream delivers exactly 30 signal events, zero error events, and a summary reporting total_signals == 30 for a valid image. Verified this test fails against the pre-fix (F-16g) code -- observed a single 'error' event with message 'Analysis failed. Please try again.' instead of 30 signal events -- and passes against the fix.cfa68d6 -
fix(F-16g): fix sse_analyzer.py's super() call broken by the composition change
Real regression caught via direct testing, not by the existing test suite (which passed despite this being broken -- see the next commit). sse_analyzer.py used super(AdvancedEnsembleDetector, detector).detect() to stream the 19 statistical signals without recursively re-running the full ensemble -- this relied on AdvancedEnsembleDetector actually inheriting from StatisticalDetector via Python's MRO. Once F-16f switched that to composition, this call raised AttributeError on every single streaming request, silently caught by this module's own broad exception handler and surfaced to users only as a generic 'Analysis failed' SSE error event -- the statistical-signals portion of every /analyze/image/stream request was completely broken. Fix: call detector._stat.detect() directly (the composed instance), matching the same pattern used throughout the rest of the F-16 change. Directly verified: reproduced the AttributeError by calling the exact broken line in isolation before writing this f…369f206 -
fix(F-16f): AdvancedEnsembleDetector composes StatisticalDetector, no longer inherits it
The actual production entry point -- and the specific class the audit called out for its confusing similarity in name/position to AdvancedAIDetector, a base class four levels down a chain this class sat at the top of. Previously: class AdvancedEnsembleDetector (StatisticalDetector), __init__ called super().__init__(...), detect() called super().detect() to get the 19-signal base_report. Now: holds self._stat = StatisticalDetector(image_bytes, filename) and calls self._stat.detect() directly -- composition, not inheritance, for the class where the audit's complaint actually matters most (this is the 'actual production entry point', not one of the four backward-compat facade classes below it). Completes F-16: no class in what was the 5-level chain inherits from another anymore. Every one of the four facade classes (previous commits) remains independently correct and testable, and this class now explicitly composes exactly one of them as one input among the 12 it combines -- rather than …80e9274 -
fix(F-16e): StatisticalDetector is now a facade, no longer inherits CovarianceDetector
Composes BasicSignals + UltraSignals + CovarianceSignals + AdvancedSignals (all 19 signals). Re-exposes all 19 methods -- test_signal_quality.py in particular calls methods from every 'level' of the old inheritance chain directly on a StatisticalDetector instance (e.g. .analyze_fft_radial_spectrum(), originally defined on the base class four levels down), which is exactly the kind of hard-to-trace-across-five-files dependency the audit flagged. The facade now makes this an explicit, flat set of delegating methods on one class instead of an inheritance-resolved chain. This is the last class in the old chain -- it no longer inherits from anything, completing the removal of the 5-level hierarchy for these four classes. The final piece is AdvancedEnsembleDetector itself, which currently still inherits from this class; the next commit fixes that. Verified: all 8 tests in test_statistical_detector.py and all 10 in test_signal_quality.py pass unchanged.d717ffb -
fix(F-16d): CovarianceDetector is now a facade, no longer inherits UltraAdvancedDetector
Composes BasicSignals + UltraSignals + CovarianceSignals. Re-exposes all 16 methods (13 'inherited' + 3 own). Verified: all 7 tests in test_covariance_detector.py pass unchanged.bbdd09d -
fix(F-16c): UltraAdvancedDetector is now a facade, no longer inherits AdvancedAIDetector
Composes BasicSignals + UltraSignals instead. Re-exposes all 13 methods (10 'inherited' + 3 own) directly, since existing callers use this class's methods the same way whether they were originally defined on it or on the old base class -- Python inheritance made this automatic before; composition makes it an explicit delegating method per signal instead. Verified: all 5 tests in test_ultra_advanced_detector.py pass unchanged.8d41abf -
fix(F-16b): AdvancedAIDetector is now a facade over statistical_signals.BasicSignals
Kept as a real, standalone class (not deleted) since existing tests construct it directly and call .detect()/individual analyze_*() methods expecting the exact original 10-signal report. No longer holds the signal-computation logic itself -- composes BasicSignals via self._signals and delegates every method to it. calculate_final_score() now calls the shared score_and_classify() helper instead of carrying its own copy of the confidence-weighted-average + boost logic. Instance attributes (cv_image, cv_gray, height, width, pil_image, image_bytes, filename) are still set directly in __init__ for backward compatibility -- test_advanced_ai_detector.py asserts on these directly (e.g. detector.cv_image is not None). Verified: all 6 tests in test_advanced_ai_detector.py pass unchanged.69c83a1 -
fix(F-16a): add statistical_signals.py -- composable signal providers
Adds the new module replacing the previous 5-level inheritance chain (AdvancedAIDetector -> UltraAdvancedDetector -> CovarianceDetector -> StatisticalDetector -> AdvancedEnsembleDetector). Contains: - ImageContext: the shared decoded-image state (image_bytes, filename, pil_image, cv_image, cv_gray, height, width) every signal needs, explicit and injectable instead of inherited instance attributes. - BasicSignals / UltraSignals / CovarianceSignals / AdvancedSignals: the four original signal groups (10 + 3 + 3 + 3 = 19 signals) as independent, composable components instead of inheritance levels. - score_and_classify(): the confidence-weighted-average + suspicious- count-boost + classification logic, extracted ONCE instead of being copy-pasted with minor variations across all four inheritance levels (this is the actual 'confidence-gating contract... enforceable in one place instead of five' the audit asked for). Every method's logic is unchanged from the original -- only …d7cf8fa
-
test(F-10): regression tests for compacted storage + the use_count race
Covers: repeated auth against one key stays at 1 line, not N; 5 keys each verified twice stay at 5 lines; revoke doesn't duplicate lines; and the core regression -- 20 threads verifying the same key concurrently must produce use_count == 20. Verified this last test fails against the pre-fix code (observed 1, 2, or 3 across 3 runs, not 20) and passes reliably against the fix.36f93c8 -
fix(F-10): compacted key storage + fix the use_count race
Two related problems fixed together since they touch the same lines: (1) data/api_keys.jsonl grew by one line per successful authentication with no bound -- _save_key() appended a full copy of the entry on every verify_key() call, not just on creation, and _load_keys() re-read and re-parsed every accumulated line on every single request. Replaced with _rewrite_keys_file(), which atomically (temp file + os.replace, atomic on POSIX and Windows) rewrites the whole store with exactly one line per key, and _upsert_key(), which does the load-merge-rewrite as one operation. (2) The use_count/last_used update was not atomic: only the final _save_key() append was lock-protected, not the read-and-increment before it, so concurrent requests using the same key could both read the same stale use_count and one increment would be silently lost. verify_key() now re-reads the latest on-disk state and increments inside the same lock acquisition as the write. Verified directly: reverted just this fix …1ad5853 -
test(F-9): regression tests for the delivery-time SSRF guard
Simulates DNS rebinding directly (target safe at registration, unsafe by delivery time) and confirms _attempt_delivery() rejects it without ever reaching the network. Verified this test fails against the pre-fix code -- it would have proceeded straight to urlopen() despite the simulated rebinding. Also covers the common case (still-safe target) to confirm the guard isn't overly strict.7ccaca2 -
fix(F-9): re-check SSRF guard at delivery time, not just registration
_reject_unsafe_webhook_target() was defined once and called exactly once, from register_webhook() -- webhook_manager.py's own docstring even carried the reminder ('Re-run at delivery time too... to defeat DNS-rebinding attacks') as a to-do that was never actually implemented. Wires the same check into _attempt_delivery(), run immediately before every delivery attempt including each retry -- closes the window where a hostname resolves safely at registration and to an internal/metadata address later. Treated as a delivery failure (not a crash) so it flows through the existing retry/suspend/logging machinery, but logged distinctly so a real rebinding attempt is identifiable and not indistinguishable from an ordinary network failure.58544a1