Spaces:
Running
Running
Commit History
-
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(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
-
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(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
-
chore(comments): trim verbose BUG FIX comment in keys.py
2f5145e -
chore(comments): trim verbose BUG FIX comment in cases.py
8cd3d0e -
chore(comments): trim verbose BUG FIX comment in webhooks.py
22b7be8 -
chore(comments): trim verbose BUG FIX comment in analyze.pyβ
Campaign E ask: the many verbose, multi-paragraph 'here's what was wrong and why' comments accumulated throughout this project belong in commit history now that the fixes are stable, not living in the file forever. Trims to a concise 1-2 line comment describing current behavior.33232ce -
fix(docs): correct Nash Equilibrium overclaim in feedback.pyβ
Same overclaim, same fix as feedback_manager.py's docstring and PHASE_ROADMAP.md's Phase 29 correction -- 2 mentions in this file (module docstring + the record_feedback endpoint's docstring).57eb88e -
fix(F-18): set MAX_IMAGE_PIXELS at import time, not first function callβ
Image.MAX_IMAGE_PIXELS = 50_000_000 used to run inside assess_image_quality(), which analyze.py imported and called locally INSIDE the request handler, AFTER the EXIF-orientation-correction block (Image.open() + exif_transpose() + .save() -- a full decode/re-encode, exactly the operation this limit exists to protect). So that earlier step ran under Pillow's default ~89-megapixel ceiling instead of this project's intended, tighter 50-megapixel one for every request before the first call to assess_image_quality() happened to occur. Moves the assignment to module level in image_quality.py (executes once at import) and changes analyze.py's import of assess_image_quality from a per-request local import to a top-of-file module import, so the limit is guaranteed to be in effect before the process ever handles a request, not just after the first one that happens to reach assess_image_quality(). Verified directly: importing backend.api.routes.analyze now changes PIL.Image.MAX_IMAGE_PIXELS froβ¦fcba924 -
fix(F-15): add Content-Length pre-check to upload.pyβ
validate_file() checked size only after file_bytes = await file.read() had already fully materialized the upload in memory. This endpoint is also the one analysis-adjacent route requiring no API key, with no route-specific rate limit beyond the global default -- so it had strictly more room to consume memory before rejection than /api/v1/analyze/image, which already has this same pre-check. Imperfect by nature (Content-Length is client-supplied and absent under chunked transfer), so validate_file()'s own post-read check remains as a second, authoritative layer -- this is defense in depth, not a replacement.c15b427 -
fix(F-5b): wire cases.py to require_role_for_methodβ
Replaces the blanket require_analyst router dependency. Fixes two concrete over/under-permission bugs the audit found: (1) an analyst key could PATCH/DELETE cases despite ROLES['analyst'] = {GET, POST} having no PATCH/DELETE; (2) a viewer key got 403 on every endpoint including plain GETs despite ROLES['viewer'] = {GET} existing specifically to allow that. Scoped to cases.py only -- analyze.py's router also carries the F-1 public-demo-key logic, and combining that with full per-method ROLES enforcement deserves its own careful pass rather than being bundled here; see REMAINING_FIXES.md.29ebb5d -
fix(F-8, F-27): sanitize report before cache/webhook/audit-log, not afterβ
Moves the NaN/Infinity sanitization pass to run immediately after generate_forensic_report() returns, before forensics_cache.set(), fire_webhooks(), and log_analysis() -- all three previously read the raw, unsanitized report; only the HTTP response (sanitized right before the return statement) was guaranteed clean. Also resolves F-27: the cache-hit branch used to carry its own near-duplicate sanitizer (_sanitize_hit) as a defensive measure against already-cached-but-unsanitized entries. Since forensics_cache is in-process/in-memory only (confirmed by reading core/cache.py -- no disk/db persistence layer) and every report is now sanitized before it ever enters the cache, that duplicate is genuinely dead weight and is removed; there is now exactly one sanitize implementation instead of two that could silently drift from each other. Also fixes the same gap in the separate /export/{fmt} endpoint (a sibling instance of the same bug in a different route, per this project's own established β¦90f67ef -
fix(F-4c): feedback.py uses shared core.auth dependenciesβ
Deletes both locally-duplicated _require_analyst and _require_admin. Completes F-4: all five routers (analyze, cases, keys, webhooks, feedback) now depend on the one implementation in backend/core/auth.py instead of four historically copy-pasted variants.47335f3 -
fix(F-4b): webhooks.py uses shared core.auth.require_adminβ
Deletes the locally-duplicated _require_admin. Its behavior already matched core.auth's implementation (this router's copy was not actually stricter/looser, just a byte-for-byte duplicate that could have silently diverged the moment either copy was edited without the other) -- this closes that risk by making it structurally impossible: one implementation, imported, not four.eeba1c1 -
fix(F-4a): keys.py uses shared core.auth.require_adminβ
Deletes the locally-duplicated _require_admin (which had ALSO already diverged from the shared implementation -- it strictly required the literal 'Bearer ' prefix and 401'd otherwise, while core.auth's version did not, meaning identical client requests behaved differently depending on which router handled them). Imported with an alias to keep every call site (_require_admin(authorization)) unchanged.c27d702 -
fix(F-1): wire analyze.py router to require_analyst_or_demoβ
The frontend has no mechanism to attach a real per-user API key (it's a public, sign-up-free static site with no server-side secret storage). require_analyst rejected every request from it with 401. Switching to require_analyst_or_demo lets the frontend authenticate with the public demo token while still requiring a real key for any other consumer.f11ae00
-
fix(security): add rate limiting to webhooks.py β previously had none at allβ
None of webhooks.py's 5 endpoints had any rate limit, unlike keys.py which correctly has one. send_test in particular triggers an on-demand outbound HTTP request with zero throttling β compounding the SSRF surface (webhook_manager.py's URL-registration SSRF fix in an earlier commit) with an unrestricted probe primitive for scanning internal addresses. Fix: instantiate a Limiter with settings.RATE_LIMIT_PER_MINUTE as the default, matching the pattern in keys.py. Endpoint-specific decorators follow (send_test rate-limited more tightly given its outbound-request side effect).03fe6c8 -
fix(config): wire RATE_LIMIT_PER_MINUTE as the Limiter's default_limitsβ
All 4 Limiter(key_func=get_remote_address) constructors had no default_limits β settings.RATE_LIMIT_PER_MINUTE (documented in .env.example and explicitly set in this project's own render.yaml) had no effect anywhere; every rate limit came from the 24 individually hardcoded @limiter.limit('N/minute') decorators. Fix: pass default_limits=[f'{RATE_LIMIT_PER_MINUTE}/minute'] to each Limiter constructor. This gives the setting real, working meaning as the FALLBACK limit for any endpoint without its own explicit decorator β the 24 intentionally-tuned per-endpoint limits are untouched, since slowapi's default_limits only applies where no route-level limit decorator is present.6ac27de -
docs(correctness): remove misleading video/PDF support claims from upload endpoint docsβ
ALLOWED_VIDEO_TYPES/ALLOWED_DOC_TYPES are empty tuples in settings β no video or PDF file can ever pass MIME validation β but the public OpenAPI/Swagger docs advertised 'Videos: MP4, MPEG' and 'Documents: PDF' as supported, actively misleading API consumers. Fix: docs now accurately state image-only support and explain why.dd04102 -
fix(security): require analyst/admin auth on ALL analyze.py and cases.py routesβ
Confirmed by grep: these two routers had zero reference to Authorization, verify_key, or any FastAPI auth dependency, despite api_key_manager.py's own _ANALYST_PATHS constant explicitly naming '/api/v1/analyze/' and '/api/v1/cases/' as needing exactly this protection. Every analysis endpoint (/image, /batch, /segment, /export, /stream, /history, /stats, /platform, /heatmap, /attribution, /c2pa, /robustness) and every case-management endpoint (create/list/search/ get/update-status/delete, add evidence) was reachable by anyone, gated only by per-IP rate limiting. Fix: dependencies=[Depends(require_analyst)] on the router constructor protects every route in one line. See next commit for the test-suite auth fixture needed so existing tests keep passing.6d75824
-
fix(api): wrap detect_segments in try/except β closes CodeQL #24β
/segment route called detect_segments() outside any try/except block. An unhandled exception propagated through FastAPI's default handler which can include stack trace details in the 500 response body. Fix: wrap the detect_segments() call in try/except, log internally with exc_info=True, and return a clean 'Segment analysis failed' message to the caller. Consistent with error handling in all other routes (/heatmap, /attribution, /export, etc.).49b98d7
-
fix: preserve JPEG format across EXIF transpose; rate-limit /segment; fix CodeQL CWE-209 (audit #7)
2bd3ac0
-
fix: circular import β revert analyze.py limiter to standalone instanceβ
Importing limiter from backend.main into analyze.py created a circular import: main.py imports analyze.py which then tried to import back from main.py. Python's module system cannot resolve partially-initialized modules in circular chains. Fix: analyze.py uses its own Limiter(key_func=get_remote_address) instance β same configuration as main.py. The two limiters track separate counters which is acceptable; the rate limiting functionality is unchanged.0b827d2 -
fix: EXIF orientation, HEIC allowed, quality confidence cap, extension validationβ
EXIF orientation (Bug 10): ImageOps.exif_transpose() now applied to file_bytes before all forensic analysis. iPhone portrait photos (EXIF rotation=6) were analyzed rotated 90 degrees, making spatial signals wrong. HEIC/TIFF allowed types (Bug 6): Route-level ALLOWED_IMAGE_TYPES now includes image/tiff, image/heic, image/heif. The lifespan registers pillow_heif but the route was rejecting HEIC with HTTP 415 before the analysis started. Quality confidence cap (Bug 7): quality['confidence_cap'] is now applied to all signal confidences after analysis. Small (low-quality) images no longer report 90% confidence on their results. Extension validation (Bug 17): validate_file() extension check result was silently discarded. Now raises FileValidationError if extension_valid is False.686a111
-
fix: methods_used fallback, HEIC extensions, CORS, Content-Length pre-checkβ
- image_forensics.py: add methods_used:[] to error fallback dict - config.py: add .tiff/.tif/.heic/.heif to ALLOWED_UPLOAD_EXTENSIONS (MIME types were accepted but extension check rejected them, making HEIC support completely unreachable) - config.py: add localhost:8000/8080/127.0.0.1:8000 to CORS_ORIGINS (developers running backend locally got CORS errors) - analyze.py: check Content-Length header before file.read() to prevent loading multi-hundred-MB uploads into RAM before rejecting them - analyze.py: replace bare str(e) in 500 HTTPException details to prevent exception info leaking to clients (CodeQL alert #24)9ad719d
-
feat: Nash Equilibrium adaptive detection feedback systemβ
- feedback_manager.py: record_feedback() applies Nash gradient update to guilty signals (w *= 1 - lr*guilt), weights clamped [0.05, 2.0], persisted to data/signal_weights.json, append-only feedback.jsonl - feedback.py: POST/GET /api/v1/feedback (analyst+admin), GET /weights (admin) - main.py: feedback router registered7b08d7d -
feat: segment-level AI detection endpointβ
- segment_detector.py: 64x64 overlapping tiles (stride 32px), per-tile ELA + DCT + noise signals, min-max normalised per-image, Gaussian-smoothed 2-D probability grid, deterministic, safe fallback for small images - analyze.py: POST /api/v1/analyze/segment endpoint44c8fb8
-
fix: sanitize numpy types, forensics isolation, admin warning, docstring, remove dotenvβ
Fix 1 β analyze.py: both _sanitize and _sanitize_hit lacked handling for numpy scalar types (np.float64, np.int32 etc.). isinstance(np.float64, float) can be True in some NumPy builds but not all. Fix: check isinstance(obj, np.generic) and call obj.item() to guarantee JSON-serializable Python scalars. Fix 2 β image_forensics.py: detect_ai_generation had no try/except around AdvancedEnsembleDetector. Any uncaught exception aborted the entire forensic report including EXIF, hashes, and tampering analysis. Fix: wrap in try/except and return a safe neutral result so the rest of the report is preserved. Fix 3 β main.py: when ADMIN_KEY_HASH env var is not set, any 16-char string grants admin access. Added explicit logger.warning so the gap is visible in logs and operators know to set the env var in production. Fix 4 β advanced_ensemble_detector.py: module docstring was after the imports as an orphaned string expression. Moved to line 1 so help() and IDEs can find it. Fix 5 β requirementsβ¦e038881
-
feat(feature): webhook delivery system with HMAC signing and retryβ
- webhook_manager.py: register, list, delete, sign, fire_webhooks - retry logic: 3 attempts at 5s / 30s / 120s back-off - auto-suspend after 3 consecutive failures - delivery log written to data/webhook_deliveries.jsonl - webhooks.py: POST / GET / DELETE /{id} / POST /{id}/test / GET /deliveries - all endpoints admin-only (mirrors keys.py auth pattern) - analyze.py: fire_webhooks() wired after forensics_cache.set() - main.py: webhooks router registered - test_webhooks.py: 17 tests (service layer + API auth gates) - VERSION 7.2.0 -> 7.3.0, version tests updatede3bc110
-
fix(analyze): remove duplicate Allowed: string in 415 error
11e4535
-
fix(ci): fix 10 test failures from previous commitsβ
audit_log.py: UnboundLocalError: 'from datetime import datetime' inside the try/with block shadowed the module-level datetime import. Python sees the local binding and raises UnboundLocalError on the datetime.now() call that precedes the assignment. Fix: remove local import, use the already-imported datetime. analyze.py: 5 endpoints returned 500 because run_in_executor(None, fn, arg1, arg2) fails when TestClient's anyio runner has no running event loop in the same thread context as get_running_loop(). Replaced all instances of await loop.run_in_executor(None, fn, *args) with await asyncio.to_thread(fn, *args) which is Python 3.9+ and works correctly in both production uvicorn and pytest-asyncio TestClient environments. Affected: heatmap, attribution, platform, c2pa, robustness, export. test_monitoring.py + test_polish.py: test_metrics_reset_endpoint and test_metrics_reset_clears_data both called POST /api/v1/metrics/reset without the X-Admin-Key heβ¦f7ee01f -
security(codeql): fix information exposure #8 #19 #20 #21β
analyze.py β CodeQL flagged f-string detail= strings that include client-controlled values (file.content_type). An attacker can set an arbitrary Content-Type header and have it reflected in the 415 error response β a potential information exposure vector. Fix: replace all detail=f'Unsupported media type: {file.content_type}' with a static string listing allowed types. Also in this commit: - NaN/Inf sanitizer now handles numpy scalars (np.float32, np.float64, np.integer) which returned False for isinstance(obj, float) and crashed JSON serialization on every analysis response - Export endpoint now checks forensics_cache before re-running the full 2-5 second analysis pipeline - All non-/image endpoints (heatmap, attribution, platform, c2pa, robustness) now offload to run_in_executor β previously all blocked the async event loop under concurrent load7c38fbd
-
fix(async): correct asyncio API + latency variable referenceβ
analyze.py β two bugs introduced by the latency/async fix: 1. asyncio.get_event_loop() is deprecated in Python 3.10+ when called from inside a running event loop (inside async def). Raises a DeprecationWarning that caused CI collection failure. Fixed: asyncio.get_event_loop() -> asyncio.get_running_loop() 2. '_latency_ms' in dir() does not reliably check local variable existence β dir() without args lists class/module scope names, not function locals. Safe to remove: cache HIT returns early before record_analysis is reached, so _latency_ms is always defined at the call site. Simplified to direct reference.8dfcd9e -
security(codeql): fix information exposure #8 #15 #16 #17 #18β
logger.error('message: %s', exc_info=True) in 8 handlers was flagged by CodeQL because the %s placeholder is in the exception data flow. Python logging treats exc_info as a keyword β %s is never filled and appears literally in logs. Remove all ': %s' format placeholders. exc_info=True already appends the full traceback to server logs. Also removes dead code: _heatmap_store dict was declared but never read or written anywhere in the file. Also adds: - Cache sanitizer on HIT path (NaN/Inf sanitize on cached results) - Actual latency measurement with time.perf_counter() - Async offload of generate_forensic_report() to thread executor so CPU-bound work doesn't block the event loop5dffe70
-
feat(sse): real-time signal waterfall via Server-Sent Eventsβ
backend/services/sse_analyzer.py (new): Async generator that streams 26-signal results as SSE events. Events: started | quality | signal | summary | error Each signal yields immediately as it completes, enabling live UI. Statistical signals streamed first, then PRNU/ELA/metadata/DCT/CLIP, then full ImageForensics report as final summary event. backend/api/routes/analyze.py: POST /api/v1/analyze/image/stream β StreamingResponse SSE endpoint 5/min rate limit, same validation as standard endpoint Headers: Cache-Control: no-cache, X-Accel-Buffering: no frontend/index.html: Signal waterfall panel with animated signal rows Suspicious signals highlighted red, clean signals green Score bar fills proportionally as each signal arrives Streaming mode is default; falls back to standard fetch analyzeFileSSE() reads SSE stream via ReadableStream API backend/tests/test_sse.py: 8 tests Endpoint exists, content-type, non-image rejection, oversized, data events present, β¦4f7d95f
-
fix(cleanup): final pyflakes cleanup across 4 filesβ
image_type_classifier.py: remove unused arr_hsv, hsv_img, colorsys import report_exporter.py: remove unused struct, datetime; fix dead y variable and add() function; fix bare f-string buf.write(f'0 6\n') batch_processor.py: remove unused hashlib, BytesIO imports analyze.py: remove unused 'import time as _time' inside metrics recorderb39683a
-
feat(monitoring): observability, inconclusive verdict, image typeβ
backend/services/image_type_classifier.py (new): First-stage content classifier before forensic scoring. Types: photo | screenshot_ui | illustration | document | low_info | unknown. Returns run_prnu/run_ela/run_metadata flags so detectors can self-gate. Rule-based, ~80% accuracy on clear cases. backend/services/metrics_collector.py (new): Thread-safe in-memory metrics: request rates, score distributions, latency percentiles (p95), classification/generator/platform breakdowns, per-signal mean scores. Rolling window of last 1000 requests. backend/main.py: GET /api/v1/metrics β real-time system metrics POST /api/v1/metrics/reset β reset counters (admin) backend/services/advanced_ensemble_detector.py: inconclusive verdict: fires when < 8 active signals or signal std > 0.35 with score in ambiguous zone (0.35-0.65). Prevents false confidence when detectors disagree or most are inactive. backend/services/image_forensics.py: image_type field added to reportβ¦30be61a -
security(codeql): fix all information exposure alerts #8 #14 #15 #16 #17β
analyze.py β remove all 'except X as e:' bindings where e was unused or flowed into logger calls that CodeQL traces as potential exposure paths: - FileValidationError (line 170) β except FileValidationError: - ValueError (line ~176) β except ValueError: with exc_info=True - IOError (line ~180) β except IOError: with exc_info=True All exception details now go to server logs via exc_info=True only, never into HTTP response detail strings.fc26634
-
fix(ci): fix 2 CI failures β quality gate wiring and platform rate limitβ
analyze.py: - Wire image quality gate (assess_image_quality) before cache lookup so tiny images get clean 422 before any analysis runs - Add NaN/Inf sanitizer (_sanitize) before returning report to prevent ValueError: Out of range float values are not JSON compliant - Remove unused 'as e' from all Exception handlers (pyflakes clean) - Remove unused 'json' import from sanitizer (math is sufficient) test_platform_detector.py: - test_platform_in_forensic_report: switch from HTTP API (rate-limited) to direct ImageForensics service call β removes 429 flakiness entirely - Remove unused pytest import test_hardening.py: - test_analyze_rejects_tiny_image: now gets clean 422 from quality gatea283c42
-
security(codeql): fix all information exposure alerts #2 #3 #5 #6 #7 #8β
analyze.py β 7 logger calls replaced f-string {e} with exc_info=True so the exception detail goes to server logs only, not into the HTTP response data flow that CodeQL tracks. Line 147 (alert #2): FileValidationError detail=str(e) replaced with generic message. str(e) from FileValidationError could contain internal path or type information. All Exception as e handlers at lines 266, 342, 377, 427, 486: logger.error(f'...{e}...') -> logger.error('...', exc_info=True) Breaks the data-flow path CodeQL traces from exception to HTTP response.35ad387
-
security(codeql): fix information exposure and missing workflow permissionsβ
analyze.py line 495 (CodeQL #8 β Information exposure through exception): Replace detail=f'Export failed: {str(e)}' with generic message. Internal exception details must not reach HTTP clients in 500 responses. Full error is still logged server-side via logger.error(..., exc_info=True). Note: line 151 detail=str(e) retained β it is FileValidationError feedback that users need to see (filename, type info). Not sensitive internal state. .github/workflows/ci.yml (CodeQL #1 β Workflow does not contain permissions): Add explicit permissions block: contents=read, security-events=write. Principle of least privilege for GitHub Actions runners.7fd4cd8