4848
4949
5050ITEMS_PER_PAGE = 20
51- OWASP_TOP10_2025_DATA_FILE = (
52- pathlib .Path (__file__ ).resolve ().parent .parent
53- / "utils"
54- / "external_project_parsers"
55- / "data"
56- / "owasp_top10_2025.json"
57- )
5851OPENCRE_STANDARD_NAME = "OpenCRE"
5952
6053app = Blueprint (
@@ -422,8 +415,12 @@ def map_analysis() -> Any:
422415 posthog .capture (f"map_analysis" , f"standards:{ standards } " )
423416
424417 database = db .Node_collection ()
418+ if len (standards ) < 2 :
419+ abort (400 , "Please provide two standards" )
420+ standards = standards [:2 ]
425421 standards_hash = gap_analysis .make_resources_key (standards )
426422
423+ # ----- PR #825: OpenCRE fast path -----
427424 if OPENCRE_STANDARD_NAME in standards :
428425 direct_gap_analysis = _build_direct_cre_overlap_map_analysis (
429426 standards , standards_hash , database
@@ -432,48 +429,69 @@ def map_analysis() -> Any:
432429 return jsonify (direct_gap_analysis )
433430 abort (404 , "No direct overlap found for requested standards" )
434431
435- # First, check if we have cached results in the database
436- if database .gap_analysis_exists (standards_hash ):
437- gap_analysis_result = database .get_gap_analysis_result (standards_hash )
438- if gap_analysis_result :
439- return jsonify (flask_json .loads (gap_analysis_result ))
440-
441- # On Heroku (read-only), check if standards exist before attempting Redis/queue operations
442- is_heroku = os .environ .get ("DYNO" ) is not None
443- if is_heroku :
444- # Check if all requested standards exist
445- try :
446- existing_standards = database .standards ()
447- if isinstance (existing_standards , (list , tuple , set )):
448- existing_lower = {str (s ).lower () for s in existing_standards }
449- missing = [s for s in standards if str (s ).lower () not in existing_lower ]
450- if missing :
451- logger .info (
452- f"On Heroku: gap analysis request { standards_hash } references "
453- f"standards that do not exist: { ', ' .join (missing )} , returning 404"
454- )
455- abort (
456- 404 , f"One or more standards do not exist: { ', ' .join (missing )} "
457- )
458- except Exception as exc :
459- # If we can't verify standards, log but don't fail (defensive)
460- logger .warning (f"Could not verify standards existence on Heroku: { exc } " )
461-
462- # If calculations are disabled, return 404
463- if os .environ .get ("CRE_NO_CALCULATE_GAP_ANALYSIS" ):
464- logger .info (
465- f"Gap analysis calculations are disabled by CRE_NO_CALCULATE_GAP_ANALYSIS; "
466- f"refusing to schedule new job for { standards_hash } "
432+ # ----- upstream: cached result -----
433+ cache_key = standards_hash
434+ if database .gap_analysis_exists (cache_key ):
435+ cached = database .get_gap_analysis_result (cache_key = cache_key )
436+ if cached :
437+ parsed = json .loads (cached )
438+ if "result" in parsed :
439+ return jsonify ({"result" : parsed .get ("result" )})
440+
441+ # ----- upstream: Heroku guard -----
442+ if os .environ .get ("HEROKU" ):
443+ abort (404 , "No such Cache" )
444+
445+ # ----- upstream: Redis / RQ path with synchronous fallback -----
446+ db_url = os .environ .get ("CRE_CACHE_FILE" ) or os .environ .get ("PROD_DATABASE_URL" )
447+ if not db_url :
448+ db_url = str (getattr (getattr (database .session , "bind" , None ), "url" , "" ))
449+ try :
450+ conn = redis .connect ()
451+ ga_queue_name = os .environ .get ("CRE_GA_QUEUE_NAME" , "ga" )
452+ q = Queue (name = ga_queue_name , connection = conn )
453+ inflight_key = f"ga:inflight:{ cache_key } "
454+ inflight_job_id_raw = conn .get (inflight_key )
455+ inflight_job_id = (
456+ inflight_job_id_raw .decode ("utf-8" )
457+ if isinstance (inflight_job_id_raw , bytes )
458+ else str (inflight_job_id_raw ) if inflight_job_id_raw else ""
467459 )
468- abort (404 , "Gap analysis calculations are disabled" )
469-
470- # Now call schedule() which will handle Redis/queue operations
471- gap_analysis_dict = gap_analysis .schedule (standards , database )
472- if "result" in gap_analysis_dict :
473- return jsonify (gap_analysis_dict )
474- if gap_analysis_dict .get ("error" ):
475- abort (404 )
476- return jsonify ({"job_id" : gap_analysis_dict .get ("job_id" )})
460+ if inflight_job_id :
461+ try :
462+ inflight_job = job .Job .fetch (id = inflight_job_id , connection = conn )
463+ if inflight_job .get_status () in (
464+ job .JobStatus .QUEUED ,
465+ job .JobStatus .STARTED ,
466+ ):
467+ return jsonify ({"job_id" : inflight_job_id })
468+ except exceptions .NoSuchJobError :
469+ conn .delete (inflight_key )
470+
471+ j = q .enqueue_call (
472+ description = f"{ standards [0 ]} ->{ standards [1 ]} " ,
473+ func = cre_main .run_gap_pair_job ,
474+ kwargs = {
475+ "importing_name" : standards [0 ],
476+ "peer_name" : standards [1 ],
477+ "db_connection_str" : db_url ,
478+ },
479+ timeout = gap_analysis .GAP_ANALYSIS_TIMEOUT ,
480+ )
481+ conn .set (inflight_key , str (j .id ))
482+ conn .expire (inflight_key , _ga_timeout_seconds ())
483+ return jsonify ({"job_id" : str (j .id )})
484+ except Exception as exc :
485+ logger .warning (
486+ "Redis/RQ unavailable in map_analysis for %s; using synchronous fallback: %s" ,
487+ cache_key ,
488+ exc ,
489+ )
490+ try :
491+ return jsonify (_compute_ga_without_redis (database , standards ))
492+ except Exception as fallback_exc :
493+ logger .exception ("Synchronous GA fallback failed for %s" , cache_key )
494+ abort (503 , f"Gap analysis unavailable: { fallback_exc } " )
477495
478496
479497@app .route ("/rest/v1/map_analysis_weak_links" , methods = ["GET" ])
@@ -1357,4 +1375,4 @@ def import_from_cre_csv() -> Any:
13571375
13581376
13591377if __name__ == "__main__" :
1360- app .run (use_reloader = False , debug = True )
1378+ app .run (use_reloader = False , debug = True )
0 commit comments