feat: geo risk scores

This commit is contained in:
Iain Learmonth 2023-10-29 15:45:10 +00:00
parent 315dae7f06
commit 0e0d499428
17 changed files with 558 additions and 54 deletions

View file

@ -1,7 +1,7 @@
import logging
from abc import abstractmethod
from datetime import datetime
from typing import Union, List, Optional, Any
from typing import Union, List, Optional, Any, Dict
from app.brm.brn import BRN
from app.extensions import db
@ -37,6 +37,21 @@ class AbstractConfiguration(db.Model): # type: ignore
]
class Deprecation(db.Model): # type: ignore[name-defined,misc]
id = db.Column(db.Integer, primary_key=True)
resource_type = db.Column(db.String(50))
resource_id = db.Column(db.Integer)
deprecated_at = db.Column(db.DateTime(), default=datetime.utcnow, nullable=False)
meta = db.Column(db.JSON())
reason = db.Column(db.String(), nullable=False)
@property
def resource(self) -> "AbstractResource":
from app.models.mirrors import Proxy
model = {'Proxy': Proxy}[self.resource_type]
return model.query.get(self.resource_id) # type: ignore[no-any-return]
class AbstractResource(db.Model): # type: ignore
__abstract__ = True
@ -72,12 +87,14 @@ class AbstractResource(db.Model): # type: ignore
def brn(self) -> BRN:
raise NotImplementedError()
def deprecate(self, *, reason: str) -> bool:
def deprecate(self, *, reason: str, meta: Optional[Dict[str, Any]] = None) -> bool:
"""
Marks the resource as deprecated. In the event that the resource was already
deprecated, no change will be recorded and the function will return False.
:param reason: an opaque string that records the deprecation reason
:param meta: metadata associated with the deprecation reason, such as the circumstances in which censorship was
detected
:return: if the proxy was deprecated
"""
if self.deprecated is None:
@ -85,10 +102,26 @@ class AbstractResource(db.Model): # type: ignore
self.deprecated = datetime.utcnow()
self.deprecation_reason = reason
self.updated = datetime.utcnow()
if reason not in [d.reason for d in self.deprecations]:
new_deprecation = Deprecation(
resource_type=type(self).__name__,
resource_id=self.id,
reason=reason,
meta=meta
)
db.session.add(new_deprecation)
return True
logging.info("Not deprecating %s (reason=%s) because it's already deprecated", self.brn, reason)
logging.info("Not deprecating %s (reason=%s) because it's already deprecated with that reason.",
self.brn, reason)
return False
@property
def deprecations(self) -> List[Deprecation]:
return Deprecation.query.filter_by( # type: ignore[no-any-return]
resource_type='Proxy',
resource_id=self.id
).all()
def destroy(self) -> None:
"""
Marks the resource for destruction.

View file

@ -1,5 +1,5 @@
import json
from datetime import datetime
from datetime import datetime, timedelta
from typing import Optional, List, Union, Any, Dict
from tldextract import extract
@ -8,9 +8,17 @@ from werkzeug.datastructures import FileStorage
from app.brm.brn import BRN
from app.brm.utils import thumbnail_uploaded_image, create_data_uri, normalize_color
from app.extensions import db
from app.models import AbstractConfiguration, AbstractResource
from app.models import AbstractConfiguration, AbstractResource, Deprecation
from app.models.onions import Onion
country_origin = db.Table(
'country_origin',
db.metadata,
db.Column('country_id', db.ForeignKey('country.id'), primary_key=True),
db.Column('origin_id', db.ForeignKey('origin.id'), primary_key=True),
extend_existing=True,
)
class Origin(AbstractConfiguration):
group_id = db.Column(db.Integer, db.ForeignKey("group.id"), nullable=False)
@ -18,9 +26,11 @@ class Origin(AbstractConfiguration):
auto_rotation = db.Column(db.Boolean, nullable=False)
smart = db.Column(db.Boolean(), nullable=False)
assets = db.Column(db.Boolean(), nullable=False)
risk_level_override = db.Column(db.Integer(), nullable=True)
group = db.relationship("Group", back_populates="origins")
proxies = db.relationship("Proxy", back_populates="origin")
countries = db.relationship("Country", secondary=country_origin, back_populates='origins')
@property
def brn(self) -> BRN:
@ -35,7 +45,7 @@ class Origin(AbstractConfiguration):
@classmethod
def csv_header(cls) -> List[str]:
return super().csv_header() + [
"group_id", "domain_name", "auto_rotation", "smart"
"group_id", "domain_name", "auto_rotation", "smart", "assets", "country"
]
def destroy(self) -> None:
@ -51,6 +61,75 @@ class Origin(AbstractConfiguration):
domain_name: str = self.domain_name
return f"https://{domain_name.replace(tld, onion.onion_name)}.onion"
@property
def risk_level(self) -> Dict[str, int]:
if self.risk_level_override:
return {country.country_code: self.risk_level_override for country in self.countries}
frequency_factor = 0
recency_factor = 0
recent_deprecations = (
db.session.query(Deprecation) # type: ignore[no-untyped-call]
.join(Proxy,
Deprecation.resource_id == Proxy.id)
.join(Origin, Origin.id == Proxy.origin_id)
.filter(
Origin.id == self.id,
Deprecation.resource_type == 'Proxy',
Deprecation.deprecated_at >= datetime.utcnow() - timedelta(hours=168)
)
.distinct(Proxy.id)
.all()
)
for deprecation in recent_deprecations:
recency_factor += 1 / max((datetime.utcnow() - deprecation.deprecated_at).total_seconds() // 3600, 1)
frequency_factor += 1
risk_levels: Dict[str, int] = {}
for country in self.countries:
risk_levels[country.country_code.upper()] = int(max(1, min(10, frequency_factor * recency_factor))) + country.risk_level
return risk_levels
class Country(AbstractConfiguration):
@property
def brn(self) -> BRN:
return BRN(
group_id=0,
product="country",
provider="iso3166-1",
resource_type="alpha2",
resource_id=self.country_code
)
country_code = db.Column(db.String(2), nullable=False)
risk_level_override = db.Column(db.Integer(), nullable=True)
origins = db.relationship("Origin", secondary=country_origin, back_populates='countries')
@property
def risk_level(self) -> int:
if self.risk_level_override:
return int(self.risk_level_override // 2)
frequency_factor = 0
recency_factor = 0
recent_deprecations = (
db.session.query(Deprecation) # type: ignore[no-untyped-call]
.join(Proxy,
Deprecation.resource_id == Proxy.id)
.join(Origin, Origin.id == Proxy.origin_id)
.join(Origin.countries)
.filter(
Country.id == self.id,
Deprecation.resource_type == 'Proxy',
Deprecation.deprecated_at >= datetime.utcnow() - timedelta(hours=168)
)
.distinct(Proxy.id)
.all()
)
for deprecation in recent_deprecations:
recency_factor += 1 / max((datetime.utcnow() - deprecation.deprecated_at).total_seconds() // 3600, 1)
frequency_factor += 1
return int(max(1, min(10, frequency_factor * recency_factor)))
class StaticOrigin(AbstractConfiguration):
group_id = db.Column(db.Integer, db.ForeignKey("group.id"), nullable=False)