2026-10-13 10:33:31 +02:00
"""Private content inspection. No external inference, raw-content logs or remote URLs."""
import base64
import hashlib
import io
import math
import os
import re
import shutil
import subprocess
import tempfile
2026-10-13 16:58:15 +02:00
import time
2026-10-13 10:33:31 +02:00
import unicodedata
import zipfile
from pathlib import Path
2026-10-13 16:58:15 +02:00
from stage_timings import timed , stage
2026-10-13 10:33:31 +02:00
os . environ [ 'HF_HUB_OFFLINE' ] = '1'
os . environ [ 'TRANSFORMERS_OFFLINE' ] = '1'
os . environ [ 'HF_HUB_DISABLE_TELEMETRY' ] = '1'
MAX_BYTES = 20 * 1024 * 1024
MAX_TEXT = 1_000_000
_MODEL = None
_OCR = None
2026-10-13 16:58:15 +02:00
@timed ( "ocr.load" )
2026-10-13 10:33:31 +02:00
def local_ocr ():
global _OCR
if _OCR is None :
import onnxruntime
onnxruntime . disable_telemetry_events ()
import rapidocr
from rapidocr import RapidOCR
directory = Path ( rapidocr . __file__ ) . parent / "models"
names = { "Det" : "ch_PP-OCRv4_det_infer.onnx" , "Rec" : "ch_PP-OCRv4_rec_infer.onnx" , "Cls" : "ch_ppocr_mobile_v2.0_cls_infer.onnx" }
checksums = { "Det" : "d2a7720d45a54257208b1e13e36a8479894cb74155a5efe29462512d42f49da9" ,
"Rec" : "48fc40f24f6d2a207a2b1091d3437eb3cc3eb6b676dc3ef9c37384005483683b" ,
"Cls" : "e47acedf663230f8863ff1ab0e64dd2d82b838fceb5957146dab185a89d6215c" }
params = { "Global.log_level" : "critical" , "EngineConfig.onnxruntime.intra_op_num_threads" : 2 ,
"EngineConfig.onnxruntime.inter_op_num_threads" : 1 }
for task , name in names . items ():
model = directory / name
if not model . is_file (): raise ValueError ( "Bundled offline OCR model unavailable" )
if hashlib . sha256 ( model . read_bytes ()) . hexdigest () != checksums [ task ]: raise ValueError ( "Offline OCR model checksum mismatch" )
params [ task + ".model_path" ] = str ( model )
# Explicit paths bypass RapidOCR's download path entirely.
_OCR = RapidOCR ( params = params )
return _OCR
2026-10-13 16:58:15 +02:00
@timed ( "yara" )
2026-10-13 10:33:31 +02:00
def scan_download ( data , policy ):
"""Malware gate only: never implies document/DLP coverage."""
result = { "decision" : "block" , "ruleId" : "yara-unavailable" , "engine" : "yara" ,
"sha256" : hashlib . sha256 ( data ) . hexdigest (), "bytes" : len ( data ),
"matchedRules" :[], "coverage" :{ "complete" : False }}
try :
config = policy . get ( "security" ,{}) . get ( "yara" ,{})
if config . get ( "enabled" ) is not True :
result [ "ruleId" ] = "yara-required" ; return result
maximum = config . get ( "maxScanBytes" , MAX_BYTES )
if not isinstance ( maximum , int ) or maximum <= 0 or len ( data ) > min ( maximum , MAX_BYTES ):
result [ "ruleId" ] = "yara-size-limit" ; return result
entries = config . get ( "rules" ,[])
if not isinstance ( entries , list ) or not 1 <= len ( entries ) <= 128 : raise ValueError ()
sources = {}; ids = {}; total = 0
for index , entry in enumerate ( entries ):
source = entry [ "source" ]; identifier = entry [ "id" ]
if not isinstance ( source , str ) or not isinstance ( identifier , str ) or not identifier : raise ValueError ()
total += len ( source . encode ( "utf-8" ))
if total > 262144 : raise ValueError ()
# No imported modules: console can leak scanned content; others expand attack surface.
if re . search ( r '\bimport\s+"' , source ): raise ValueError ()
namespace = "policy_" + str ( index ); sources [ namespace ] = source ; ids [ namespace ] = identifier
import yara
rules = yara . compile ( sources = sources , includes = False , error_on_warning = True )
matches = rules . match ( data = data , timeout = 5 )
2026-10-13 16:58:15 +02:00
if config . get ( "scanOfficeMacros" , True ) is not False :
from office_macros import macro_payloads
try :
with stage ( "macros" ):
summary , payloads = macro_payloads ( data )
result [ "officeMacros" ] = summary
deadline = time . monotonic () + 10
for payload in payloads :
if time . monotonic () >= deadline : raise ValueError ( 'macro-scan-timeout' )
matches . extend ( rules . match ( data = payload , timeout = 5 ))
except Exception :
result [ "ruleId" ] = "office-macro-inspection-failed"
result [ "officeMacros" ] = { "office" : data . startswith ( b ' \xd0\xcf\x11\xe0 ' ) or data . startswith ( b 'PK' ),
"present" : False , "modules" : 0 , "complete" : False }
return result
else :
result [ "officeMacros" ] = { "office" : False , "present" : False , "modules" : 0 , "complete" : False }
2026-10-13 10:33:31 +02:00
result [ "coverage" ][ "complete" ] = True
2026-10-13 16:58:15 +02:00
result [ "matchedRules" ] = [{ "id" : identifier , "rule" : rule } for identifier , rule in
dict . fromkeys (( ids [ item . namespace ], item . rule ) for item in matches )]
2026-10-13 10:33:31 +02:00
if matches : result [ "ruleId" ] = ids [ matches [ 0 ] . namespace ]
else : result [ "decision" ] = "allow" ; result [ "ruleId" ] = None
except ImportError : pass
except Exception : result [ "ruleId" ] = "yara-inspection-failed"
return result
2026-10-13 16:58:15 +02:00
@timed ( "inspect" )
2026-10-13 10:33:31 +02:00
def reference_document ( data ):
"""Return privacy-preserving registration features, without extracted plaintext."""
result = { "sha256" : hashlib . sha256 ( data ) . hexdigest (), "bytes" : len ( data ),
"contentType" : content_kind ( data ), "fragmentHashes" :[], "coverage" :{ "complete" : False }}
text = "" ; media = []; hashes = set ()
try :
if len ( data ) > MAX_BYTES : raise ValueError ()
text , complete , media = extract ( data , result [ "contentType" ])
if len ( text ) > MAX_TEXT : raise ValueError ()
hashes = fragment_hashes ( text )
result [ "fragmentHashes" ] = sorted ( hashes )[: 1000 ]
truncated = len ( hashes ) > 1000
result [ "coverage" ] = { "complete" : complete and not truncated , "extractionComplete" : complete ,
"featuresComplete" : not truncated , "fragmentHashesTruncated" : truncated ,
"fragmentHashesTotal" : len ( hashes ), "fragmentHashesRetained" : len ( result [ "fragmentHashes" ]),
"textCharacters" : len ( text ), "mediaItems" : len ( media )}
result [ "registrationReady" ] = bool ( complete and not truncated and ( hashes or text . strip ()))
except Exception : result [ "registrationReady" ] = False
finally :
# Release transient content references; immutable Python strings cannot be securely wiped.
text = "" ; media . clear (); hashes . clear (); data = None
return result
def fragment_hashes ( text ):
normalized = unicodedata . normalize ( "NFKC" , text ) . lower () . replace ( "ß" , "ss" ) . replace ( "ς" , "σ " )
words = re . findall ( r "\w+" , normalized )
# Every shingle matches excerpts even when their starting offset changes.
hashes = { hashlib . sha256 ( " " . join ( words [ index : index + 8 ]) . encode ()) . hexdigest ()
for index in range ( max ( 0 , len ( words ) - 7 ))}
for paragraph in re . split ( r "\n\s*\n" , normalized ):
paragraph = " " . join ( paragraph . split ())
if len ( paragraph ) >= 32 : hashes . add ( hashlib . sha256 ( paragraph . encode ()) . hexdigest ())
return hashes
def cosine ( left , right ):
if len ( left ) != len ( right ) or not left :
raise ValueError ( "Embedding dimensions do not match" )
if not all ( math . isfinite ( float ( v )) for v in [ * left , * right ]):
raise ValueError ( "Invalid embedding" )
denominator = math . sqrt ( sum ( v * v for v in left )) * math . sqrt ( sum ( v * v for v in right ))
if not denominator :
raise ValueError ( "Empty embedding" )
return sum ( a * b for a , b in zip ( left , right )) / denominator
def content_kind ( data ):
if data . startswith ( b "%PDF-" ): return "pdf"
if data . startswith ( b "PK \x03\x04 " ): return "office"
if data . startswith ( b " \x89 PNG \r\n\x1a\n " ) or data . startswith ( b " \xff\xd8\xff " ) or data . startswith (( b "GIF87a" , b "GIF89a" )): return "image"
if data . startswith ( b "RIFF" ):
return { b "WAVE" : "audio" , b "WEBP" : "image" , b "AVI " : "video" } . get ( data [ 8 : 12 ], "unknown" )
if data . startswith (( b "ID3" , b "fLaC" , b "OggS" )): return "audio"
if len ( data ) > 12 and data [ 4 : 8 ] == b "ftyp" : return "video"
try :
text = data . decode ( "utf-8" , errors = "strict" )
if " \x00 " in text : return "unknown"
return "text"
except UnicodeError : return "unknown"
2026-10-13 16:58:15 +02:00
@timed ( "ocr" )
2026-10-13 10:33:31 +02:00
def ocr_image ( data ):
from PIL import Image , ImageSequence
image = Image . open ( io . BytesIO ( data ))
frames = getattr ( image , "n_frames" , 1 )
if image . width * image . height > 20_000_000 or frames > 32 or image . width * image . height * frames > 50_000_000 :
raise ValueError ( "Image processing budget exceeded" )
if os . environ . get ( "SAFETYNET_OCR_ENGINE" , "rapidocr" ) == "rapidocr" :
import numpy as np
engine = local_ocr (); parts = []
for frame in ImageSequence . Iterator ( image ):
output = engine ( np . asarray ( frame . convert ( "RGB" )))
parts . extend ( output . txts or [])
return " \n " . join ( parts ), True
configured = os . environ . get ( "TESSERACT_PATH" , "" )
executable = configured if configured and Path ( configured ) . is_file () else shutil . which ( "tesseract" )
if not executable : return "" , False
parts = []
with tempfile . TemporaryDirectory ( prefix = "safetynet-ocr-" ) as temp :
for index , frame in enumerate ( ImageSequence . Iterator ( image )):
file = Path ( temp ) / f "frame- { index } .png"
frame . convert ( "RGB" ) . save ( file )
result = subprocess . run ([ executable , str ( file ), "stdout" , "--psm" , "3" ], capture_output = True , timeout = 20 , check = True )
parts . append ( result . stdout . decode ( "utf-8" ))
return " \n " . join ( parts ), True
2026-10-13 16:58:15 +02:00
@timed ( "parse" )
2026-10-13 10:33:31 +02:00
def extract ( data , kind ):
if kind == "text" : return data . decode ( "utf-8" ), True , []
if kind == "image" :
text , complete = ocr_image ( data )
return text , complete ,[( "image" , data )]
if kind == "pdf" :
from pypdf import PdfReader
reader = PdfReader ( io . BytesIO ( data ), strict = True )
if reader . is_encrypted : raise ValueError ( "Encrypted PDF cannot be inspected" )
if len ( reader . pages ) > 100 : raise ValueError ( "PDF page budget exceeded" )
text = []; complete = True ; media = []
for page in reader . pages :
value = page . extract_text () or ""
text . append ( value )
images = list ( page . images )
if not value . strip () and not images : complete = False
for image in images :
if len ( image . data ) > MAX_BYTES : raise ValueError ( "PDF image budget exceeded" )
scanned , ok = ocr_image ( image . data ); text . append ( scanned ); complete = complete and ok
media . append (( "image" , image . data ))
if len ( media ) > 100 : raise ValueError ( "PDF media budget exceeded" )
# Attachments, JavaScript, forms, actions and vector drawings need deeper inspection.
if page . get ( "/Annots" ): complete = False
root = reader . trailer [ "/Root" ]
if root . get ( "/AcroForm" ) or root . get ( "/OpenAction" ) or root . get ( "/Names" ): complete = False
# Text extraction is useful, but does not prove all visible/vector PDF content was covered.
return " \n " . join ( text ), False , media
if kind == "office" :
from defusedxml.ElementTree import fromstring
text = []; media = []; complete = True
with zipfile . ZipFile ( io . BytesIO ( data )) as archive :
members = archive . infolist ()
if len ( members ) > 2000 or sum ( x . file_size for x in members ) > MAX_BYTES :
raise ValueError ( "Archive expansion budget exceeded" )
if not any ( x . filename in [ "word/document.xml" , "ppt/presentation.xml" , "xl/workbook.xml" ] for x in members ):
raise ValueError ( "Only OOXML document archives are supported" )
for entry in members :
if entry . flag_bits & 1 or entry . file_size > MAX_BYTES or entry . file_size > max ( entry . compress_size , 1 ) * 200 :
raise ValueError ( "Unsafe document archive" )
name = entry . filename
if any ( s in name . lower () for s in [ "vbaproject" , "embeddings/" , "activex/" , "customxml/" ]): complete = False
if name . endswith ( ".xml" ):
root = fromstring ( archive . read ( entry ))
text . extend ( element . text for element in root . iter () if element . tag . rsplit ( "}" , 1 )[ - 1 ] in [ "t" , "v" ] and element . text )
elif "/media/" in name :
payload = archive . read ( entry )
if content_kind ( payload ) != "image" : complete = False ; continue
scanned , ok = ocr_image ( payload ); text . append ( scanned ); complete = complete and ok ; media . append (( "image" , payload ))
if len ( media ) > 100 : raise ValueError ( "Office media budget exceeded" )
# Charts, relationships, equations and rendered layout are not yet fully inspected.
return " \n " . join ( text ), False , media
if kind in [ "audio" , "video" ]: return "" , False ,[( kind , data )]
raise ValueError ( "Unsupported content type" )
2026-10-13 16:58:15 +02:00
@timed ( "semantic.load" )
2026-10-13 10:33:31 +02:00
def get_model ():
global _MODEL
directory = os . environ . get ( "EMBEDDING_MODEL_DIR" , "" )
worker = os . environ . get ( "EMBEDDING_WORKER_PATH" , "" )
if not directory or not Path ( directory ) . is_dir () or not worker : raise ValueError ( "Verified local EmbeddingGemma 2 runtime is not configured" )
if _MODEL is None :
from semantic import NativeSemanticModel
_MODEL = NativeSemanticModel ( directory , worker )
return _MODEL
2026-10-13 16:58:15 +02:00
@timed ( "semantic" )
2026-10-13 10:33:31 +02:00
def embed ( data , kind ):
model = get_model ()
if kind == "text" : vectors = model . text_vectors ( data . decode ( "utf-8" ))
elif kind == "image" : vectors = model . image_vectors ( data )
elif kind == "audio" : vectors = model . audio_vectors ( data )
else : raise ValueError ( "Native semantic modality is not configured" )
return aggregate_vectors ( vectors )
def aggregate_vectors ( vectors ):
from semantic import validate_vectors , DIMENSIONS
validate_vectors ( vectors )
vector = [ sum ( item [ index ] for item in vectors ) / len ( vectors ) for index in range ( DIMENSIONS )]
norm = math . sqrt ( sum ( value * value for value in vector ))
if norm < 1e-12 : raise ValueError ( "Degenerate semantic aggregate" )
return [ value / norm for value in vector ]
2026-10-13 16:58:15 +02:00
@timed ( "semantic" )
2026-10-13 10:33:31 +02:00
def semantic_vector_features ( text , media ):
model = get_model (); vectors = model . text_vectors ( text ) if text . strip () else []
text_chunks = len ( vectors )
for kind , payload in media :
if kind == "image" : vectors . extend ( model . image_vectors ( payload ))
elif kind == "audio" : vectors . extend ( model . audio_vectors ( payload ))
else : raise ValueError ( "Native semantic modality is not configured" )
if len ( vectors ) > 64 : raise ValueError ( "Semantic vector budget exceeded" )
if not vectors : raise ValueError ( "No semantic inputs" )
return vectors , text_chunks
def semantic_vectors ( text , media ):
return semantic_vector_features ( text , media )[ 0 ]
2026-10-13 16:58:15 +02:00
@timed ( "semantic" )
2026-10-13 10:33:31 +02:00
def embedding_features ( data ):
kind = content_kind ( data )
if len ( data ) > MAX_BYTES : raise ValueError ( "Embedding input budget exceeded" )
text , complete , media = extract ( data , kind )
if len ( text ) > MAX_TEXT : raise ValueError ( "Extracted text budget exceeded" )
vectors , text_chunks = semantic_vector_features ( text , media )
return { ** get_model () . info (), "vector" : aggregate_vectors ( vectors ), "vectors" : vectors ,
"coverage" :{ "complete" : complete , "extractionComplete" : complete , "textChunks" : text_chunks , "mediaItems" : len ( media )}}
def deterministic ( text , data , policy ):
digest = hashlib . sha256 ( data ) . hexdigest ()
normalized = unicodedata . normalize ( "NFKC" , text ) . casefold ()
patterns = [( "private-key" , r "-----BEGIN (?:RSA |EC |OPENSSH |DSA )?PRIVATE KEY-----" ),
( "aws-access-key" , r "\b(?:AKIA|ASIA)[A-Z0-9] {16} \b" ),
( "api-secret" , r "\b(?:sk-[A-Za-z0-9_-]{20,}|gh[pousr]_[A-Za-z0-9]{20,})\b" )]
for rule , pattern in patterns :
if re . search ( pattern , text ): return rule
fragments = None
for rule in policy [ "dlp" ][ "rules" ]:
if rule [ "kind" ] == "sha256" and rule [ "value" ] . lower () == digest : return rule [ "id" ]
if rule [ "kind" ] == "literal" and unicodedata . normalize ( "NFKC" , rule [ "value" ]) . casefold () in normalized : return rule [ "id" ]
if rule [ "kind" ] == "fragment-sha256" and text :
if fragments is None : fragments = fragment_hashes ( text )
if rule [ "value" ] . lower () in fragments : return rule [ "id" ]
return None
2026-10-13 16:58:15 +02:00
@timed ( "inspect" )
2026-10-13 10:33:31 +02:00
def inspect_content ( data , policy , encoder = embed ):
kind = content_kind ( data )
result = { "decision" : "block" , "ruleId" : "uninspectable" , "contentType" : kind , "coverage" :{ "complete" : False }, "model" : None }
if len ( data ) > min ( MAX_BYTES , policy [ "dlp" ][ "maxBytes" ]): result [ "ruleId" ] = "size-limit" ; return result
if policy . get ( "security" ,{}) . get ( "yara" ,{}) . get ( "enabled" ):
malware = scan_download ( data , policy )
if malware [ "decision" ] != "allow" : return { ** result , "ruleId" : malware [ "ruleId" ], "malware" : malware }
# Fingerprints can block any bytes before invoking parsers.
rule = deterministic ( "" , data , policy )
if rule : result [ "ruleId" ] = rule ; return result
try :
text , complete , media = extract ( data , kind )
if len ( text ) > MAX_TEXT : raise ValueError ( "Extracted text budget exceeded" )
result [ "coverage" ] = { "complete" : complete , "textCharacters" : len ( text ), "mediaItems" : len ( media )}
rule = deterministic ( text , data , policy )
if rule : result [ "ruleId" ] = rule ; return result
semantic = policy [ "dlp" ] . get ( "semantic" ,{})
if semantic . get ( "enabled" ):
if encoder is embed :
info = get_model () . info ()
if any ( semantic . get ( key ) != info [ key ] for key in [ "model" , "revision" , "profile" , "dimensions" , "artifactFormat" , "quantization" , "runtimeRevision" ]): raise ValueError ( "Pinned model profile unavailable" )
elif semantic . get ( "model" ) != "google/embeddinggemma-2" or semantic . get ( "revision" ) != os . environ . get ( "EMBEDDING_MODEL_REVISION" ): raise ValueError ( "Pinned test model revision unavailable" )
if not semantic . get ( "categories" ): raise ValueError ( "Semantic policy has no reference categories" )
result [ "model" ] = semantic [ "model" ]; result [ "similarity" ] = []
if encoder is embed : vectors = semantic_vectors ( text , media )
else :
inputs = [( "text" , text [ i : i + 4000 ] . encode ()) for i in range ( 0 , len ( text ), 3500 )] + media
if not inputs or len ( inputs ) > 64 : raise ValueError ( "Inference budget exceeded" )
vectors = [ encoder ( payload , modality ) for modality , payload in inputs ]
if sum ( len ( category . get ( "prototypes" ) or [ category [ "prototype" ]]) for category in semantic [ "categories" ]) > 256 : raise ValueError ( "Semantic reference budget exceeded" )
for category in semantic [ "categories" ]:
prototypes = category . get ( "prototypes" ) or [ category [ "prototype" ]]
if not isinstance ( category . get ( "threshold" ),( int , float )) or not math . isfinite ( category [ "threshold" ]) or not 0 < category [ "threshold" ] <= 1 : raise ValueError ( "Invalid semantic threshold" )
scores = [ cosine ( vector , prototype ) for vector in vectors for prototype in prototypes ]
score = max ( scores ); result [ "similarity" ] . append ({ "categoryId" : category [ "id" ], "score" : round ( score , 6 )})
if score >= category [ "threshold" ]: result [ "ruleId" ] = category [ "id" ]; return result
if complete : result [ "decision" ] = "allow" ; result [ "ruleId" ] = None
return result
except Exception :
# Parser/model errors never release content and never serialize raw exception text.
result [ "ruleId" ] = "inspection-incomplete" ; return result