montana/Русский/Логистика/maritime_compliance.py

502 lines
21 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""
Maritime Compliance Module Sanctions, AIS Anomaly, Dark Fleet Detection
Extracted from marinetraffic_parser.py for modularity.
All functions are self-contained with no dependencies on routing/ports/scraping.
"""
import logging
from datetime import datetime
from typing import Dict
logger = logging.getLogger('maritime_compliance')
# =============================================================================
# SANCTIONED FLAGS & ENTITIES
# =============================================================================
# Sanctioned flag states (OFAC, EU, UN — as of 2025-2026)
SANCTIONED_FLAGS = {
'north korea': {'level': 'critical', 'lists': ['OFAC SDN', 'UN', 'EU'], 'code': 'KP'},
'dprk': {'level': 'critical', 'lists': ['OFAC SDN', 'UN', 'EU'], 'code': 'KP'},
'iran': {'level': 'critical', 'lists': ['OFAC SDN', 'EU'], 'code': 'IR'},
'syria': {'level': 'critical', 'lists': ['OFAC SDN', 'EU'], 'code': 'SY'},
'cuba': {'level': 'high', 'lists': ['OFAC SDN'], 'code': 'CU'},
'crimea': {'level': 'high', 'lists': ['OFAC', 'EU'], 'code': '-'},
'russia': {'level': 'elevated', 'lists': ['OFAC sectoral', 'EU partial'], 'code': 'RU'},
'venezuela': {'level': 'elevated', 'lists': ['OFAC sectoral'], 'code': 'VE'},
'myanmar': {'level': 'elevated', 'lists': ['OFAC targeted', 'EU targeted'], 'code': 'MM'},
'belarus': {'level': 'elevated', 'lists': ['OFAC', 'EU'], 'code': 'BY'},
}
# High-risk company keywords (sanctioned entities)
SANCTIONED_ENTITIES = [
'irisl', 'national iranian', 'cosco dalian', 'cosco shipping (dalian)',
'ocean mighty', 'glory wealth', 'dark fleet', 'patriot',
'korea ocean shipping', 'pyongyang', 'korea kumgang',
'syria petroleum', 'sytrol',
'rosneft trading', 'sovcomflot', # Partial sanctions
'pdvsa', 'petroleos de venezuela',
]
# Dark fleet indicators
DARK_FLEET_INDICATORS = [
'ais_gaps', # Frequent AIS switch-off
'flag_changes', # Multiple flag changes in short period
'sts_transfers', # Ship-to-ship transfers in suspicious areas
'age_25plus', # Vessel age > 25 years (tankers)
'unknown_owner', # Opaque ownership structure
]
# =============================================================================
# STS TRANSFER HOTSPOTS
# =============================================================================
# High-risk STS transfer zones
STS_HOTSPOTS = {
'south_laconia_greece': {'lat_range': (36, 37), 'lon_range': (22, 24)},
'off_ceuta': {'lat_range': (35, 36), 'lon_range': (-6, -5)},
'malacca_strait': {'lat_range': (1, 4), 'lon_range': (101, 104)},
'off_lomé_togo': {'lat_range': (5, 7), 'lon_range': (0, 2)},
'east_malaysia': {'lat_range': (4, 7), 'lon_range': (110, 115)},
}
# =============================================================================
# HELPER FUNCTIONS
# =============================================================================
def _max_risk(current: str, new: str) -> str:
"""Return the higher of two risk levels."""
levels = ['clear', 'low', 'elevated', 'high', 'critical']
return new if levels.index(new) > levels.index(current) else current
# =============================================================================
# SANCTIONS & COMPLIANCE SCREENER
# =============================================================================
def screen_sanctions(vessel_name: str = None, imo: str = None, flag: str = None,
companies: list = None, vessel_type: str = None,
year_built: int = None) -> Dict:
"""
Screen vessel/company against sanctions lists.
Returns risk assessment with level, flags, and recommendations.
"""
risks = []
risk_level = 'clear' # clear → low → elevated → high → critical
# 1. Flag screening
if flag:
flag_lower = flag.lower().strip()
for sanctioned, info in SANCTIONED_FLAGS.items():
if sanctioned in flag_lower or info['code'].lower() == flag_lower:
risks.append({
'type': 'sanctioned_flag',
'severity': info['level'],
'detail': f"Flag state '{flag}' is on {', '.join(info['lists'])} sanctions lists",
'lists': info['lists'],
})
risk_level = _max_risk(risk_level, info['level'])
# 2. Company screening
if companies:
for company in companies:
c_name = ''
if isinstance(company, dict):
c_name = (company.get('name', '') or '').lower()
elif isinstance(company, str):
c_name = company.lower()
for entity in SANCTIONED_ENTITIES:
if entity in c_name:
risks.append({
'type': 'sanctioned_entity',
'severity': 'high',
'detail': f"Company '{c_name}' matches sanctioned entity pattern '{entity}'",
})
risk_level = _max_risk(risk_level, 'high')
break
# 3. Dark fleet indicators
dark_fleet_flags = []
if vessel_type and vessel_type.lower() in ('tanker', 'oil tanker', 'crude oil tanker'):
if year_built:
try:
age = 2026 - int(year_built)
if age >= 25:
dark_fleet_flags.append('age_25plus')
risks.append({
'type': 'dark_fleet_indicator',
'severity': 'elevated',
'detail': f"Vessel is {age} years old — common in dark/shadow fleet operations",
})
risk_level = _max_risk(risk_level, 'elevated')
elif age >= 20:
dark_fleet_flags.append('aging_vessel')
risks.append({
'type': 'dark_fleet_indicator',
'severity': 'low',
'detail': f"Vessel is {age} years old — monitor for fleet age concerns",
})
except (ValueError, TypeError):
pass
# 4. Name-based heuristics (common in sanctioned fleets)
if vessel_name:
vn = vessel_name.lower()
# Frequent name changes (vessels with very generic names)
generic_names = ['glory', 'success', 'fortune', 'lucky', 'star', 'ocean', 'sea', 'diamond', 'golden']
generic_count = sum(1 for g in generic_names if g in vn)
if generic_count >= 2:
risks.append({
'type': 'name_pattern',
'severity': 'low',
'detail': 'Vessel name contains multiple generic keywords — common in dark fleet renaming patterns',
})
# Build result
result = {
'vessel_name': vessel_name,
'imo': imo,
'flag': flag,
'risk_level': risk_level,
'risk_count': len(risks),
'risks': risks,
'screened_against': ['OFAC SDN', 'OFAC Sectoral', 'EU Consolidated', 'UN Security Council'],
}
if risk_level == 'clear':
result['recommendation'] = 'No sanctions flags detected. Standard due diligence applies.'
elif risk_level == 'low':
result['recommendation'] = 'Minor flags noted. Proceed with enhanced due diligence.'
elif risk_level == 'elevated':
result['recommendation'] = 'Elevated risk. Obtain legal counsel before proceeding. Check latest OFAC/EU advisories.'
elif risk_level == 'high':
result['recommendation'] = 'HIGH RISK — sanctioned entity match. Do NOT proceed without legal clearance. Report to compliance.'
elif risk_level == 'critical':
result['recommendation'] = 'CRITICAL — vessel/entity appears on primary sanctions lists. DO NOT ENGAGE. Report to compliance officer immediately.'
return result
# =============================================================================
# AIS ANOMALY DETECTOR
# =============================================================================
def detect_ais_anomalies(vessel_name: str = None, imo: str = None,
mmsi: str = None, flag: str = None,
vessel_type: str = None, year_built: int = None,
last_known_lat: float = None, last_known_lon: float = None,
ais_gap_hours: float = None) -> Dict:
"""Detect AIS behavioral anomalies for compliance screening."""
anomalies = []
risk_score = 0 # 0-100
# 1. AIS gap detection
if ais_gap_hours and ais_gap_hours > 12:
severity = 'critical' if ais_gap_hours > 72 else 'high' if ais_gap_hours > 48 else 'elevated' if ais_gap_hours > 24 else 'moderate'
risk_score += min(30, int(ais_gap_hours / 2))
anomalies.append({
'type': 'ais_gap',
'severity': severity,
'detail': f"AIS signal gap of {ais_gap_hours:.0f} hours detected",
'implication': 'Possible intentional AIS switch-off — common indicator of sanctions evasion or illicit STS transfer',
})
# 2. Location in STS hotspot
if last_known_lat is not None and last_known_lon is not None:
for zone_name, zone in STS_HOTSPOTS.items():
lat_min, lat_max = zone['lat_range']
lon_min, lon_max = zone['lon_range']
if lat_min <= last_known_lat <= lat_max and lon_min <= last_known_lon <= lon_max:
risk_score += 20
anomalies.append({
'type': 'sts_hotspot',
'severity': 'high',
'detail': f"Vessel located in known STS transfer zone: {zone_name.replace('_', ' ').title()}",
'implication': 'Ship-to-ship transfers in this area are flagged for potential sanctions evasion',
})
# 3. Flag risk
if flag:
flag_lower = flag.lower()
for sanctioned, info in SANCTIONED_FLAGS.items():
if sanctioned in flag_lower:
risk_score += 15
anomalies.append({
'type': 'flag_risk',
'severity': info['level'],
'detail': f"Flag state '{flag}' on {', '.join(info['lists'])} lists",
'implication': 'Enhanced due diligence required for all commercial engagements',
})
break
# 4. Vessel age risk (for tankers especially)
if year_built and vessel_type:
age = datetime.now().year - year_built
vtype = vessel_type.lower()
if 'tanker' in vtype and age > 20:
risk_score += 10
anomalies.append({
'type': 'aged_tanker',
'severity': 'elevated',
'detail': f"Tanker vessel aged {age} years (built {year_built})",
'implication': 'Aged tankers are commonly used in dark fleet operations. Check P&I coverage and ownership history.',
})
# 5. Spoofing indicators (if position seems wrong for vessel type)
if last_known_lat is not None and last_known_lon is not None:
if abs(last_known_lat) > 80:
risk_score += 15
anomalies.append({
'type': 'position_anomaly',
'severity': 'high',
'detail': f"Position reported in extreme latitude ({last_known_lat:.1f}\u00b0)",
'implication': 'Possible AIS spoofing or GPS manipulation',
})
# Determine overall risk level
if risk_score >= 60:
risk_level = 'critical'
elif risk_score >= 40:
risk_level = 'high'
elif risk_score >= 20:
risk_level = 'elevated'
elif risk_score > 0:
risk_level = 'low'
else:
risk_level = 'clear'
recommendations = []
if risk_level in ('critical', 'high'):
recommendations.extend([
'Do NOT engage commercially without enhanced due diligence',
'Verify vessel ownership chain and beneficial owner',
'Check P&I club status and insurance coverage',
'Request recent port state control inspection reports',
'Monitor for further AIS anomalies over next 7 days',
])
elif risk_level == 'elevated':
recommendations.extend([
'Enhanced monitoring recommended',
'Verify P&I and H&M insurance status',
'Request vessel particulars and ownership documentation',
])
else:
recommendations.append('No significant anomalies detected — standard monitoring sufficient')
return {
'vessel': vessel_name or 'Unknown',
'imo': imo or '',
'mmsi': mmsi or '',
'flag': flag or '',
'risk_score': risk_score,
'risk_level': risk_level,
'anomalies_found': len(anomalies),
'anomalies': anomalies,
'recommendations': recommendations,
'screened_against': [
'AIS gap detection (>12h threshold)',
'STS transfer hotspot zones (5 regions)',
'Sanctions flag screening (OFAC/EU/UN)',
'Dark fleet age profiling',
'Position anomaly / spoofing detection',
],
'note': 'AIS anomaly assessment based on available data points. For real-time monitoring, integrate with live AIS feed (Spire, Kpler, MarineTraffic).',
}
# =============================================================================
# DARK FLEET DETECTOR
# =============================================================================
def detect_dark_fleet(vessel_name: str = None, imo: str = None,
flag: str = None, year_built: int = None,
owner: str = None, vessel_type: str = None,
dwt: float = None, previous_flags: list = None,
p_and_i_club: str = None, ais_gaps_30d: int = None) -> Dict:
"""Screen vessel for dark/shadow fleet indicators."""
indicators = []
score = 0 # 0-100
vtype = (vessel_type or '').lower()
age = (datetime.now().year - year_built) if year_built else 0
# 1. Vessel age (critical for tankers)
if age > 25 and 'tanker' in vtype:
score += 20
indicators.append({
'indicator': 'Aged tanker (>25 years)',
'severity': 'high',
'detail': f"Built {year_built}, age {age} years. Tankers >20 years are predominant in dark fleet.",
'weight': 20,
})
elif age > 20 and 'tanker' in vtype:
score += 12
indicators.append({
'indicator': 'Aging tanker (>20 years)',
'severity': 'elevated',
'detail': f"Built {year_built}, age {age} years. Approaching dark fleet age profile.",
'weight': 12,
})
elif age > 15:
score += 5
indicators.append({
'indicator': 'Older vessel',
'severity': 'low',
'detail': f"Built {year_built}, age {age} years.",
'weight': 5,
})
# 2. Flag state risk
if flag:
flag_lower = flag.lower()
for sanctioned, info in SANCTIONED_FLAGS.items():
if sanctioned in flag_lower:
weight = {'critical': 25, 'high': 20, 'elevated': 10}.get(info['level'], 5)
score += weight
indicators.append({
'indicator': f"Sanctioned flag state ({flag})",
'severity': info['level'],
'detail': f"On {', '.join(info['lists'])} lists. Dark fleet commonly uses sanctioned or FOC flags.",
'weight': weight,
})
break
else:
# Flags of Convenience (FOC) commonly used by dark fleet
foc_flags = ['cameroon', 'palau', 'gabon', 'togo', 'tanzania', 'comoros', 'equatorial guinea', 'sao tome']
if flag_lower in foc_flags:
score += 15
indicators.append({
'indicator': f"High-risk Flag of Convenience ({flag})",
'severity': 'elevated',
'detail': 'Flag commonly associated with dark fleet operations and limited regulatory oversight.',
'weight': 15,
})
# 3. Flag changes
if previous_flags and len(previous_flags) >= 3:
score += 15
indicators.append({
'indicator': f"Multiple flag changes ({len(previous_flags)} flags)",
'severity': 'high',
'detail': f"Previous flags: {', '.join(previous_flags)}. Frequent flag-hopping is a dark fleet indicator.",
'weight': 15,
})
# 4. P&I club status
if p_and_i_club:
reputable_clubs = ['gard', 'britannia', 'standard', 'steamship mutual', 'north', 'skuld',
'swedish', 'west of england', 'japan', 'london', 'american', 'shipowners', 'uk p&i']
if not any(club in p_and_i_club.lower() for club in reputable_clubs):
score += 15
indicators.append({
'indicator': f"Non-IG P&I club ({p_and_i_club})",
'severity': 'elevated',
'detail': 'Not a member of International Group of P&I Clubs. Dark fleet vessels often lack reputable P&I coverage.',
'weight': 15,
})
else:
score += 10
indicators.append({
'indicator': 'P&I club status unknown',
'severity': 'moderate',
'detail': 'Unable to verify P&I coverage. Vessels without proper insurance are high risk.',
'weight': 10,
})
# 5. Ownership opacity
if owner:
owner_lower = owner.lower()
suspicious_keywords = ['single purpose', 'one ship', 'special purpose', 'unknown', 'nominee']
if any(kw in owner_lower for kw in suspicious_keywords):
score += 15
indicators.append({
'indicator': 'Opaque ownership structure',
'severity': 'high',
'detail': f"Owner '{owner}' suggests single-purpose entity — common dark fleet structure to limit liability.",
'weight': 15,
})
# Check sanctioned entities
for entity in SANCTIONED_ENTITIES:
if entity in owner_lower:
score += 25
indicators.append({
'indicator': f"Owner matches sanctioned entity",
'severity': 'critical',
'detail': f"Owner '{owner}' matches known sanctioned entity pattern.",
'weight': 25,
})
break
# 6. AIS behavior
if ais_gaps_30d and ais_gaps_30d > 5:
score += 15
indicators.append({
'indicator': f"Frequent AIS gaps ({ais_gaps_30d} in 30 days)",
'severity': 'high',
'detail': 'Frequent AIS switch-offs are primary indicator of sanctions-evading dark fleet operations.',
'weight': 15,
})
# Overall assessment
score = min(100, score)
if score >= 70:
risk_level = 'critical'
classification = 'HIGH PROBABILITY dark fleet vessel'
elif score >= 50:
risk_level = 'high'
classification = 'LIKELY dark fleet / shadow fleet vessel'
elif score >= 30:
risk_level = 'elevated'
classification = 'POSSIBLE dark fleet — enhanced due diligence required'
elif score >= 10:
risk_level = 'low'
classification = 'LOW risk — minor indicators present'
else:
risk_level = 'clear'
classification = 'No dark fleet indicators detected'
compliance = {
'can_engage': risk_level in ('clear', 'low'),
'requires_due_diligence': risk_level in ('elevated',),
'do_not_engage': risk_level in ('high', 'critical'),
}
return {
'vessel': vessel_name or 'Unknown',
'imo': imo or '',
'vessel_type': vtype or 'unknown',
'dwt': dwt,
'flag': flag or 'Unknown',
'year_built': year_built,
'age_years': age,
'dark_fleet_score': score,
'risk_level': risk_level,
'classification': classification,
'indicators_found': len(indicators),
'indicators': indicators,
'compliance_status': compliance,
'screened_against': [
'Vessel age profiling (>20yr tankers)',
'OFAC/EU/UN sanctions flag screening',
'Flag-hopping detection',
'P&I club verification (IG membership)',
'Beneficial ownership opacity check',
'Sanctioned entity matching',
'AIS behavior analysis',
],
'recommendation': (
'DO NOT ENGAGE — vessel shows multiple dark fleet indicators. Report to compliance team.'
if risk_level in ('critical', 'high') else
'ENHANCED DUE DILIGENCE required before engagement. Verify ownership, insurance, and recent trading history.'
if risk_level == 'elevated' else
'Standard due diligence sufficient. Monitor for changes.'
),
'note': 'Dark fleet screening based on publicly available indicators. For comprehensive screening, cross-reference with Windward, Kpler, or IHS Markit databases.',
}