@@ -608,7 +608,9 @@ def _post(url, headers=None, data=None, json=None, timeout=10):
608608 'verify' : DEFAULT_VERIFY_TLS ,
609609 'tls_low' : DEFAULT_TLS_LOW ,
610610 'auth_scheme' : None ,
611- 'session' : None
611+ 'session' : None ,
612+ 'raw_fallback' : False ,
613+ 'event_gzip_supported' : None ,
612614}
613615
614616# Batch mode controls
@@ -788,32 +790,102 @@ def _send_batch(lines: list, is_json: bool, product: str):
788790 POST = _CONNECTION_CACHE ['session' ]
789791 headers_auth = {** HEADERS }
790792 headers_auth ["Authorization" ] = f"{ _CONNECTION_CACHE ['auth_scheme' ]} { HEC_TOKEN } "
791- # Both endpoints use text/plain with gzip for batched events
793+
794+ def _raise_with_batch_diag (resp_obj , target_url : str ):
795+ try :
796+ resp_obj .raise_for_status ()
797+ except Exception :
798+ if _VERBOSITY in ('info' , 'verbose' , 'debug' ):
799+ body_preview = (resp_obj .text or "" )[:500 ]
800+ print (f"[BATCH] HTTP { resp_obj .status_code } from { target_url } " , flush = True )
801+ if body_preview :
802+ print (f"[BATCH] Error body: { body_preview } " , flush = True )
803+ sys .stdout .flush ()
804+ raise
805+
806+ def _post_event_line_with_fallback (line_str : str , target_url : str ):
807+ line_bytes = line_str .encode ('utf-8' )
808+ gzip_supported = _CONNECTION_CACHE .get ('event_gzip_supported' )
809+
810+ # Try gzip first unless we've already learned this endpoint rejects it.
811+ if gzip_supported is not False :
812+ gz_line = gzip .compress (line_bytes , compresslevel = 1 )
813+ gz_headers = {** headers_auth , "Content-Type" : "application/json" , "Content-Encoding" : "gzip" }
814+ gz_resp = POST (target_url , headers = gz_headers , data = gz_line , timeout = 30 )
815+ try :
816+ gz_resp .raise_for_status ()
817+ _CONNECTION_CACHE ['event_gzip_supported' ] = True
818+ return
819+ except Exception :
820+ # Fall back to plain JSON for content/parse/media-type failures.
821+ status = getattr (gz_resp , "status_code" , None )
822+ if status not in (400 , 415 , 422 ):
823+ _raise_with_batch_diag (gz_resp , target_url )
824+ if _CONNECTION_CACHE .get ('event_gzip_supported' ) is not False and _VERBOSITY in ('info' , 'verbose' , 'debug' ):
825+ print ("[BATCH] /event gzip rejected, falling back to plain JSON" , flush = True )
826+ sys .stdout .flush ()
827+
828+ plain_headers = {** headers_auth , "Content-Type" : "application/json" }
829+ plain_resp = POST (target_url , headers = plain_headers , data = line_bytes , timeout = 30 )
830+ _raise_with_batch_diag (plain_resp , target_url )
831+ _CONNECTION_CACHE ['event_gzip_supported' ] = False
792832 body = "\n " .join (lines ).encode ('utf-8' )
793-
794- # Use fast compression (level 1) for high throughput - trades compression ratio for speed
795- # Level 1 is ~10x faster than default level 9, with only ~10% larger output
833+
834+ # Use fast compression (level 1) for high throughput on /raw batches.
835+ # For /event batches, send plain JSON to maximize compatibility with
836+ # pipeline front-ends that reject gzip-encoded /event payloads.
796837 gz = gzip .compress (body , compresslevel = 1 )
797- headers = {** headers_auth , "Content-Type" : "text/plain" , "Content-Encoding" : "gzip" }
838+
839+ if _VERBOSITY in ('info' , 'verbose' , 'debug' ):
840+ print (f"[BATCH] Flushing { len (lines )} events ({ len (body )} bytes raw)" , flush = True )
841+ sys .stdout .flush ()
798842
799843 if is_json :
800- # JSON products to /event endpoint
844+ # JSON products to /event endpoint.
845+ # Send each queued event line individually (not NDJSON body).
846+ # Try gzip first; if rejected, auto-fallback to plain JSON.
801847 url = _CONNECTION_CACHE ['event_base' ]
848+ for line in lines :
849+ _post_event_line_with_fallback (line , url )
850+ if _VERBOSITY == 'debug' :
851+ print (f"[BATCH] Sent { len (lines )} /event items successfully" , flush = True )
852+ sys .stdout .flush ()
853+ return
854+ elif not _CONNECTION_CACHE ['raw_fallback' ]:
855+ # Raw products: try /raw first
856+ raw_url = f"{ _CONNECTION_CACHE ['raw_base' ]} ?{ _build_qs (product )} "
857+ raw_headers = {** headers_auth , "Content-Type" : "text/plain" , "Content-Encoding" : "gzip" }
858+ try :
859+ resp = POST (raw_url , headers = raw_headers , data = gz , timeout = 30 )
860+ resp .raise_for_status ()
861+ if _VERBOSITY == 'debug' :
862+ print (f"[BATCH] Response: { resp .status_code } - { resp .text [:200 ] if resp .text else 'OK' } " , flush = True )
863+ sys .stdout .flush ()
864+ return
865+ except Exception as raw_err :
866+ if _VERBOSITY in ('info' , 'verbose' , 'debug' ):
867+ print (f"[BATCH] /raw failed ({ type (raw_err ).__name__ } : { raw_err } ), switching to /event with _raw wrapper" , flush = True )
868+ sys .stdout .flush ()
869+ _CONNECTION_CACHE ['raw_fallback' ] = True
870+ # Fall through to _raw JSON wrapper below
871+ event_lines = [json .dumps (_envelope ({"_raw" : l }, product , {}, None ), separators = ("," , ":" )) for l in lines ]
872+ url = _CONNECTION_CACHE ['event_base' ]
873+ for line in event_lines :
874+ _post_event_line_with_fallback (line , url )
875+ if _VERBOSITY == 'debug' :
876+ print (f"[BATCH] Sent { len (event_lines )} /event+_raw items successfully" , flush = True )
877+ sys .stdout .flush ()
878+ return
802879 else :
803- # Raw/syslog products to /raw endpoint
804- url = f"{ _CONNECTION_CACHE ['raw_base' ]} ?{ _build_qs (product )} "
805-
806- # Show batch flush in info mode and above (not debug only)
807- if _VERBOSITY in ('info' , 'verbose' , 'debug' ):
808- print (f"[BATCH] Flushing { len (lines )} events ({ len (gz )} bytes compressed)" , flush = True )
809- sys .stdout .flush ()
810-
811- resp = POST (url , headers = headers , data = gz , timeout = 30 )
812- resp .raise_for_status ()
813-
814- if _VERBOSITY == 'debug' :
815- print (f"[BATCH] Response: { resp .status_code } - { resp .text [:200 ] if resp .text else 'OK' } " , flush = True )
816- sys .stdout .flush ()
880+ # raw_fallback already cached — go straight to /event with _raw wrapper
881+ event_lines = [json .dumps (_envelope ({"_raw" : l }, product , {}, None ), separators = ("," , ":" )) for l in lines ]
882+ url = _CONNECTION_CACHE ['event_base' ]
883+ for line in event_lines :
884+ _post_event_line_with_fallback (line , url )
885+ if _VERBOSITY == 'debug' :
886+ print (f"[BATCH] Sent { len (event_lines )} /event+_raw items successfully" , flush = True )
887+ sys .stdout .flush ()
888+ return
817889
818890SOURCETYPE_MAP_OVERRIDES = {
819891 # ===== FIXED PARSER MAPPINGS (Based on actual parser directory names) =====
@@ -1196,15 +1268,16 @@ def send_one(line, product: str, attr_fields: dict, event_time: float | None = N
11961268 env_raw = base + "/raw"
11971269 bases = []
11981270 if env_event and env_raw :
1271+ # Explicit URL configured — use only that; don't fall back to other regions
1272+ # (tokens are region-specific and cross-region fallback would also fail)
11991273 bases .append ((env_event , env_raw ))
1200- bases .extend ([
1201- ("https://ingest.us1.sentinelone.net/services/collector/event" ,
1202- "https://ingest.us1.sentinelone.net/services/collector/raw" ),
1203- ("https://ingest.usea1.sentinelone.net/services/collector/event" ,
1204- "https://ingest.usea1.sentinelone.net/services/collector/raw" ),
1205- ("https://ingest.sentinelone.net/services/collector/event" ,
1206- "https://ingest.sentinelone.net/services/collector/raw" ),
1207- ])
1274+ else :
1275+ bases .extend ([
1276+ ("https://ingest.us1.sentinelone.net/services/collector/event" ,
1277+ "https://ingest.us1.sentinelone.net/services/collector/raw" ),
1278+ ("https://ingest.usea1.sentinelone.net/services/collector/event" ,
1279+ "https://ingest.usea1.sentinelone.net/services/collector/raw" ),
1280+ ])
12081281
12091282 # Try verification/TLS combinations (secure → low TLS → insecure as last resort)
12101283 combos = [
@@ -1251,6 +1324,12 @@ def send_one(line, product: str, attr_fields: dict, event_time: float | None = N
12511324 payload = _envelope (line , product , attr_fields , event_time )
12521325 headers = {** headers_auth , "Content-Type" : "application/json" }
12531326 resp = POST (url , headers = headers , json = payload , timeout = 10 )
1327+ elif _CONNECTION_CACHE ['raw_fallback' ]:
1328+ url = _CONNECTION_CACHE ['event_base' ]
1329+ raw_str = line if isinstance (line , str ) else json .dumps (line , separators = ("," , ":" ))
1330+ payload = _envelope ({"_raw" : raw_str }, product , attr_fields , event_time )
1331+ headers = {** headers_auth , "Content-Type" : "application/json" }
1332+ resp = POST (url , headers = headers , json = payload , timeout = 10 )
12541333 else :
12551334 url = f"{ _CONNECTION_CACHE ['raw_base' ]} ?{ _build_qs (product )} "
12561335 payload = line
@@ -1323,6 +1402,43 @@ def send_one(line, product: str, attr_fields: dict, event_time: float | None = N
13231402 # On SSL/connection errors, continue to next combo/base
13241403 continue
13251404
1405+ # For raw products: retry all bases using /event with _raw JSON wrapper as fallback
1406+ if product not in JSON_PRODUCTS :
1407+ for event_base , raw_base in bases :
1408+ for verify , tls_low in combos :
1409+ POST = _make_poster (verify = verify , tls_low = tls_low )
1410+ for scheme in auth_schemes :
1411+ headers_auth = {** HEADERS }
1412+ headers_auth ["Authorization" ] = f"{ scheme } { HEC_TOKEN } "
1413+ raw_str = line if isinstance (line , str ) else json .dumps (line , separators = ("," , ":" ))
1414+ try :
1415+ url = event_base
1416+ payload = _envelope ({"_raw" : raw_str }, product , attr_fields , event_time )
1417+ headers = {** headers_auth , "Content-Type" : "application/json" }
1418+ if DEBUG :
1419+ print (f"[DEBUG] Sending to { url } (_raw fallback)" )
1420+ resp = POST (url , headers = headers , json = payload , timeout = 10 )
1421+ if resp .status_code in (401 , 403 ) and scheme == auth_schemes [0 ]:
1422+ continue
1423+ resp .raise_for_status ()
1424+ _CONNECTION_CACHE ['configured' ] = True
1425+ _CONNECTION_CACHE ['event_base' ] = event_base
1426+ _CONNECTION_CACHE ['raw_base' ] = raw_base
1427+ _CONNECTION_CACHE ['verify' ] = verify
1428+ _CONNECTION_CACHE ['tls_low' ] = tls_low
1429+ _CONNECTION_CACHE ['auth_scheme' ] = scheme
1430+ _CONNECTION_CACHE ['session' ] = POST
1431+ _CONNECTION_CACHE ['raw_fallback' ] = True
1432+ if _VERBOSITY in ('info' , 'verbose' , 'debug' ):
1433+ print (f"[INFO] /raw not supported, using /event+_raw wrapper for { product } " , flush = True )
1434+ try :
1435+ return resp .json ()
1436+ except ValueError :
1437+ return {"status" : "OK" , "code" : resp .status_code }
1438+ except Exception as e :
1439+ last_error = e
1440+ continue
1441+
13261442 # If all attempts failed, raise last error for visibility
13271443 if last_error :
13281444 raise last_error
0 commit comments