feat: geo risk scores
This commit is contained in:
parent
315dae7f06
commit
0e0d499428
17 changed files with 558 additions and 54 deletions
|
@ -1,12 +1,15 @@
|
||||||
# pylint: disable=too-few-public-methods
|
# pylint: disable=too-few-public-methods
|
||||||
|
|
||||||
import builtins
|
import builtins
|
||||||
|
from datetime import datetime, timedelta
|
||||||
from typing import Dict, List, Union, Optional
|
from typing import Dict, List, Union, Optional
|
||||||
|
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
from sqlalchemy import or_
|
||||||
from tldextract import extract
|
from tldextract import extract
|
||||||
|
|
||||||
|
from app.extensions import db
|
||||||
from app.models.base import Group, Pool
|
from app.models.base import Group, Pool
|
||||||
from app.models.mirrors import Proxy
|
from app.models.mirrors import Proxy
|
||||||
|
|
||||||
|
@ -17,6 +20,9 @@ class MMMirror(BaseModel):
|
||||||
origin_domain_root: str = Field(description="The registered domain name of the origin, excluding subdomains")
|
origin_domain_root: str = Field(description="The registered domain name of the origin, excluding subdomains")
|
||||||
valid_from: str = Field(description="The date on which the mirror was added to the system")
|
valid_from: str = Field(description="The date on which the mirror was added to the system")
|
||||||
valid_to: Optional[str] = Field(description="The date on which the mirror was decommissioned")
|
valid_to: Optional[str] = Field(description="The date on which the mirror was decommissioned")
|
||||||
|
# countries: List[Tuple[str, int]] = Field(description="A list mapping of risk levels to country")
|
||||||
|
country: Optional[str] = Field(description="The country code of the country in which the origin is targeted")
|
||||||
|
risk: int = Field(description="A risk score for the origin in the target country")
|
||||||
|
|
||||||
|
|
||||||
class MirrorMapping(BaseModel):
|
class MirrorMapping(BaseModel):
|
||||||
|
@ -35,17 +41,40 @@ class MirrorMapping(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
def mirror_mapping(_: Optional[Pool]) -> Dict[str, Union[str, Dict[str, str]]]:
|
def mirror_mapping(_: Optional[Pool]) -> Dict[str, Union[str, Dict[str, str]]]:
|
||||||
|
one_week_ago = datetime.utcnow() - timedelta(days=7)
|
||||||
|
|
||||||
|
proxies = (
|
||||||
|
db.session.query(Proxy) # type: ignore[no-untyped-call]
|
||||||
|
.filter(or_(Proxy.destroyed.is_(None), Proxy.destroyed > one_week_ago))
|
||||||
|
.filter(Proxy.url.is_not(None))
|
||||||
|
.all()
|
||||||
|
)
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
for proxy in proxies:
|
||||||
|
if proxy.origin.countries: # Check if there are any associated countries
|
||||||
|
risk_levels = proxy.origin.risk_level.items()
|
||||||
|
highest_risk_country = max(risk_levels, key=lambda x: x[1])
|
||||||
|
highest_risk_country_code = highest_risk_country[0]
|
||||||
|
highest_risk_level = highest_risk_country[1]
|
||||||
|
else:
|
||||||
|
highest_risk_country_code = "ZZ"
|
||||||
|
highest_risk_level = 0
|
||||||
|
|
||||||
|
result[proxy.url.lstrip("https://")] = MMMirror(
|
||||||
|
origin_domain=proxy.origin.domain_name,
|
||||||
|
origin_domain_normalized=proxy.origin.domain_name.replace("www.", ""),
|
||||||
|
origin_domain_root=extract(proxy.origin.domain_name).registered_domain,
|
||||||
|
valid_from=proxy.added.isoformat(),
|
||||||
|
valid_to=proxy.destroyed.isoformat() if proxy.destroyed is not None else None,
|
||||||
|
# countries=[], # TODO: countries,
|
||||||
|
country=highest_risk_country_code,
|
||||||
|
risk=highest_risk_level
|
||||||
|
)
|
||||||
|
|
||||||
return MirrorMapping(
|
return MirrorMapping(
|
||||||
version="1.1",
|
version="1.2",
|
||||||
mappings={
|
mappings=result,
|
||||||
d.url.lstrip("https://"): MMMirror(
|
|
||||||
origin_domain=d.origin.domain_name,
|
|
||||||
origin_domain_normalized=d.origin.domain_name.replace("www.", ""),
|
|
||||||
origin_domain_root=extract(d.origin.domain_name).registered_domain,
|
|
||||||
valid_from=d.added.isoformat(),
|
|
||||||
valid_to=d.destroyed.isoformat() if d.destroyed is not None else None
|
|
||||||
) for d in Proxy.query.all() if d.url is not None
|
|
||||||
},
|
|
||||||
s3_buckets=[
|
s3_buckets=[
|
||||||
f"{current_app.config['GLOBAL_NAMESPACE']}-{g.group_name.lower()}-logs-cloudfront"
|
f"{current_app.config['GLOBAL_NAMESPACE']}-{g.group_name.lower()}-logs-cloudfront"
|
||||||
for g in Group.query.filter(Group.destroyed.is_(None)).all()
|
for g in Group.query.filter(Group.destroyed.is_(None)).all()
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import logging
|
import logging
|
||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
from datetime import datetime
|
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.brm.brn import BRN
|
||||||
from app.extensions import db
|
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
|
class AbstractResource(db.Model): # type: ignore
|
||||||
__abstract__ = True
|
__abstract__ = True
|
||||||
|
|
||||||
|
@ -72,12 +87,14 @@ class AbstractResource(db.Model): # type: ignore
|
||||||
def brn(self) -> BRN:
|
def brn(self) -> BRN:
|
||||||
raise NotImplementedError()
|
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
|
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.
|
deprecated, no change will be recorded and the function will return False.
|
||||||
|
|
||||||
:param reason: an opaque string that records the deprecation reason
|
: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
|
:return: if the proxy was deprecated
|
||||||
"""
|
"""
|
||||||
if self.deprecated is None:
|
if self.deprecated is None:
|
||||||
|
@ -85,10 +102,26 @@ class AbstractResource(db.Model): # type: ignore
|
||||||
self.deprecated = datetime.utcnow()
|
self.deprecated = datetime.utcnow()
|
||||||
self.deprecation_reason = reason
|
self.deprecation_reason = reason
|
||||||
self.updated = datetime.utcnow()
|
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
|
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
|
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:
|
def destroy(self) -> None:
|
||||||
"""
|
"""
|
||||||
Marks the resource for destruction.
|
Marks the resource for destruction.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import json
|
import json
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
from typing import Optional, List, Union, Any, Dict
|
from typing import Optional, List, Union, Any, Dict
|
||||||
|
|
||||||
from tldextract import extract
|
from tldextract import extract
|
||||||
|
@ -8,9 +8,17 @@ from werkzeug.datastructures import FileStorage
|
||||||
from app.brm.brn import BRN
|
from app.brm.brn import BRN
|
||||||
from app.brm.utils import thumbnail_uploaded_image, create_data_uri, normalize_color
|
from app.brm.utils import thumbnail_uploaded_image, create_data_uri, normalize_color
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.models import AbstractConfiguration, AbstractResource
|
from app.models import AbstractConfiguration, AbstractResource, Deprecation
|
||||||
from app.models.onions import Onion
|
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):
|
class Origin(AbstractConfiguration):
|
||||||
group_id = db.Column(db.Integer, db.ForeignKey("group.id"), nullable=False)
|
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)
|
auto_rotation = db.Column(db.Boolean, nullable=False)
|
||||||
smart = db.Column(db.Boolean(), nullable=False)
|
smart = db.Column(db.Boolean(), nullable=False)
|
||||||
assets = 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")
|
group = db.relationship("Group", back_populates="origins")
|
||||||
proxies = db.relationship("Proxy", back_populates="origin")
|
proxies = db.relationship("Proxy", back_populates="origin")
|
||||||
|
countries = db.relationship("Country", secondary=country_origin, back_populates='origins')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def brn(self) -> BRN:
|
def brn(self) -> BRN:
|
||||||
|
@ -35,7 +45,7 @@ class Origin(AbstractConfiguration):
|
||||||
@classmethod
|
@classmethod
|
||||||
def csv_header(cls) -> List[str]:
|
def csv_header(cls) -> List[str]:
|
||||||
return super().csv_header() + [
|
return super().csv_header() + [
|
||||||
"group_id", "domain_name", "auto_rotation", "smart"
|
"group_id", "domain_name", "auto_rotation", "smart", "assets", "country"
|
||||||
]
|
]
|
||||||
|
|
||||||
def destroy(self) -> None:
|
def destroy(self) -> None:
|
||||||
|
@ -51,6 +61,75 @@ class Origin(AbstractConfiguration):
|
||||||
domain_name: str = self.domain_name
|
domain_name: str = self.domain_name
|
||||||
return f"https://{domain_name.replace(tld, onion.onion_name)}.onion"
|
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):
|
class StaticOrigin(AbstractConfiguration):
|
||||||
group_id = db.Column(db.Integer, db.ForeignKey("group.id"), nullable=False)
|
group_id = db.Column(db.Integer, db.ForeignKey("group.id"), nullable=False)
|
||||||
|
|
|
@ -14,6 +14,7 @@ from app.models.bridges import Bridge
|
||||||
from app.models.mirrors import Origin, Proxy
|
from app.models.mirrors import Origin, Proxy
|
||||||
from app.models.base import Group
|
from app.models.base import Group
|
||||||
from app.models.onions import Eotk
|
from app.models.onions import Eotk
|
||||||
|
from app.portal.country import bp as country
|
||||||
from app.portal.cloud import bp as cloud
|
from app.portal.cloud import bp as cloud
|
||||||
from app.portal.automation import bp as automation
|
from app.portal.automation import bp as automation
|
||||||
from app.portal.bridgeconf import bp as bridgeconf
|
from app.portal.bridgeconf import bp as bridgeconf
|
||||||
|
@ -35,6 +36,7 @@ portal.register_blueprint(automation, url_prefix="/automation")
|
||||||
portal.register_blueprint(bridgeconf, url_prefix="/bridgeconf")
|
portal.register_blueprint(bridgeconf, url_prefix="/bridgeconf")
|
||||||
portal.register_blueprint(bridge, url_prefix="/bridge")
|
portal.register_blueprint(bridge, url_prefix="/bridge")
|
||||||
portal.register_blueprint(cloud, url_prefix="/cloud")
|
portal.register_blueprint(cloud, url_prefix="/cloud")
|
||||||
|
portal.register_blueprint(country, url_prefix="/country")
|
||||||
portal.register_blueprint(eotk, url_prefix="/eotk")
|
portal.register_blueprint(eotk, url_prefix="/eotk")
|
||||||
portal.register_blueprint(group, url_prefix="/group")
|
portal.register_blueprint(group, url_prefix="/group")
|
||||||
portal.register_blueprint(list_, url_prefix="/list")
|
portal.register_blueprint(list_, url_prefix="/list")
|
||||||
|
|
74
app/portal/country.py
Normal file
74
app/portal/country.py
Normal file
|
@ -0,0 +1,74 @@
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import sqlalchemy
|
||||||
|
from flask import Blueprint, render_template, Response, flash
|
||||||
|
from flask.typing import ResponseReturnValue
|
||||||
|
from flask_wtf import FlaskForm
|
||||||
|
from wtforms import IntegerField, BooleanField, SubmitField
|
||||||
|
|
||||||
|
from app.extensions import db
|
||||||
|
from app.models.mirrors import Country
|
||||||
|
|
||||||
|
bp = Blueprint("country", __name__)
|
||||||
|
|
||||||
|
_SECTION_TEMPLATE_VARS = {
|
||||||
|
"section": "country",
|
||||||
|
"help_url": "https://bypass.censorship.guide/user/countries.html"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@bp.app_template_filter("country_flag")
|
||||||
|
def filter_country_flag(country_code: str) -> str:
|
||||||
|
country_code = country_code.upper()
|
||||||
|
|
||||||
|
# Calculate the regional indicator symbol for each letter in the country code
|
||||||
|
base = ord('\U0001F1E6') - ord('A')
|
||||||
|
flag = ''.join([chr(ord(char) + base) for char in country_code])
|
||||||
|
|
||||||
|
return flag
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/list')
|
||||||
|
def country_list() -> ResponseReturnValue:
|
||||||
|
countries = Country.query.filter(Country.destroyed.is_(None)).all()
|
||||||
|
print(len(countries))
|
||||||
|
return render_template("list.html.j2",
|
||||||
|
title="Countries",
|
||||||
|
item="country",
|
||||||
|
new_link=None,
|
||||||
|
items=sorted(countries, key=lambda x: x.country_code),
|
||||||
|
**_SECTION_TEMPLATE_VARS
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class EditCountryForm(FlaskForm): # type: ignore[misc]
|
||||||
|
risk_level_override = BooleanField("Force Risk Level Override?")
|
||||||
|
risk_level_override_number = IntegerField("Forced Risk Level", description="Number from 0 to 20", default=0)
|
||||||
|
submit = SubmitField('Save Changes')
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/edit/<country_id>', methods=['GET', 'POST'])
|
||||||
|
def country_edit(country_id: int) -> ResponseReturnValue:
|
||||||
|
country = Country.query.filter(Country.id == country_id).first()
|
||||||
|
if country is None:
|
||||||
|
return Response(render_template("error.html.j2",
|
||||||
|
section="country",
|
||||||
|
header="404 Country Not Found",
|
||||||
|
message="The requested country could not be found."),
|
||||||
|
status=404)
|
||||||
|
form = EditCountryForm(risk_level_override=country.risk_level_override is not None,
|
||||||
|
risk_level_override_number=country.risk_level_override)
|
||||||
|
if form.validate_on_submit():
|
||||||
|
if form.risk_level_override.data:
|
||||||
|
country.risk_level_override = form.risk_level_override_number.data
|
||||||
|
else:
|
||||||
|
country.risk_level_override = None
|
||||||
|
country.updated = datetime.utcnow()
|
||||||
|
try:
|
||||||
|
db.session.commit()
|
||||||
|
flash("Saved changes to country.", "success")
|
||||||
|
except sqlalchemy.exc.SQLAlchemyError:
|
||||||
|
flash("An error occurred saving the changes to the country.", "danger")
|
||||||
|
return render_template("country.html.j2",
|
||||||
|
section="country",
|
||||||
|
country=country, form=form)
|
|
@ -3,17 +3,18 @@ from datetime import datetime
|
||||||
from typing import Optional, List
|
from typing import Optional, List
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
import sqlalchemy
|
||||||
from flask import flash, redirect, url_for, render_template, Response, Blueprint
|
from flask import flash, redirect, url_for, render_template, Response, Blueprint
|
||||||
from flask.typing import ResponseReturnValue
|
from flask.typing import ResponseReturnValue
|
||||||
from flask_wtf import FlaskForm
|
from flask_wtf import FlaskForm
|
||||||
from sqlalchemy import exc
|
from sqlalchemy import exc
|
||||||
from wtforms import StringField, SelectField, SubmitField, BooleanField
|
from wtforms import StringField, SelectField, SubmitField, BooleanField, IntegerField
|
||||||
from wtforms.validators import DataRequired
|
from wtforms.validators import DataRequired
|
||||||
|
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.models.base import Group
|
from app.models.base import Group
|
||||||
from app.models.mirrors import Origin
|
from app.models.mirrors import Origin, Country
|
||||||
from app.portal.util import response_404, view_lifecycle
|
from app.portal.util import response_404, view_lifecycle, LifecycleForm
|
||||||
|
|
||||||
bp = Blueprint("origin", __name__)
|
bp = Blueprint("origin", __name__)
|
||||||
|
|
||||||
|
@ -28,15 +29,22 @@ class NewOriginForm(FlaskForm): # type: ignore
|
||||||
submit = SubmitField('Save Changes')
|
submit = SubmitField('Save Changes')
|
||||||
|
|
||||||
|
|
||||||
class EditOriginForm(FlaskForm): # type: ignore
|
class EditOriginForm(FlaskForm): # type: ignore[misc]
|
||||||
description = StringField('Description', validators=[DataRequired()])
|
description = StringField('Description', validators=[DataRequired()])
|
||||||
group = SelectField('Group', validators=[DataRequired()])
|
group = SelectField('Group', validators=[DataRequired()])
|
||||||
auto_rotate = BooleanField("Enable auto-rotation?")
|
auto_rotate = BooleanField("Enable auto-rotation?")
|
||||||
smart_proxy = BooleanField("Requires smart proxy?")
|
smart_proxy = BooleanField("Requires smart proxy?")
|
||||||
asset_domain = BooleanField("Used to host assets for other domains?", default=False)
|
asset_domain = BooleanField("Used to host assets for other domains?", default=False)
|
||||||
|
risk_level_override = BooleanField("Force Risk Level Override?")
|
||||||
|
risk_level_override_number = IntegerField("Forced Risk Level", description="Number from 0 to 20", default=0)
|
||||||
submit = SubmitField('Save Changes')
|
submit = SubmitField('Save Changes')
|
||||||
|
|
||||||
|
|
||||||
|
class CountrySelectForm(FlaskForm): # type: ignore[misc]
|
||||||
|
country = SelectField("Country", validators=[DataRequired()])
|
||||||
|
submit = SubmitField('Save Changes', render_kw={"class": "btn btn-success"})
|
||||||
|
|
||||||
|
|
||||||
def final_domain_name(domain_name: str) -> str:
|
def final_domain_name(domain_name: str) -> str:
|
||||||
session = requests.Session()
|
session = requests.Session()
|
||||||
r = session.get(f"https://{domain_name}/", allow_redirects=True, timeout=10)
|
r = session.get(f"https://{domain_name}/", allow_redirects=True, timeout=10)
|
||||||
|
@ -84,7 +92,9 @@ def origin_edit(origin_id: int) -> ResponseReturnValue:
|
||||||
description=origin.description,
|
description=origin.description,
|
||||||
auto_rotate=origin.auto_rotation,
|
auto_rotate=origin.auto_rotation,
|
||||||
smart_proxy=origin.smart,
|
smart_proxy=origin.smart,
|
||||||
asset_domain=origin.assets)
|
asset_domain=origin.assets,
|
||||||
|
risk_level_override=origin.risk_level_override is not None,
|
||||||
|
risk_level_override_number=origin.risk_level_override)
|
||||||
form.group.choices = [(x.id, x.group_name) for x in Group.query.all()]
|
form.group.choices = [(x.id, x.group_name) for x in Group.query.all()]
|
||||||
if form.validate_on_submit():
|
if form.validate_on_submit():
|
||||||
origin.group_id = form.group.data
|
origin.group_id = form.group.data
|
||||||
|
@ -92,12 +102,16 @@ def origin_edit(origin_id: int) -> ResponseReturnValue:
|
||||||
origin.auto_rotation = form.auto_rotate.data
|
origin.auto_rotation = form.auto_rotate.data
|
||||||
origin.smart = form.smart_proxy.data
|
origin.smart = form.smart_proxy.data
|
||||||
origin.assets = form.asset_domain.data
|
origin.assets = form.asset_domain.data
|
||||||
|
if form.risk_level_override.data:
|
||||||
|
origin.risk_level_override = form.risk_level_override_number.data
|
||||||
|
else:
|
||||||
|
origin.risk_level_override = None
|
||||||
origin.updated = datetime.utcnow()
|
origin.updated = datetime.utcnow()
|
||||||
try:
|
try:
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
flash("Saved changes to group.", "success")
|
flash(f"Saved changes for origin {origin.domain_name}.", "success")
|
||||||
except exc.SQLAlchemyError:
|
except exc.SQLAlchemyError:
|
||||||
flash("An error occurred saving the changes to the group.", "danger")
|
flash("An error occurred saving the changes to the origin.", "danger")
|
||||||
return render_template("origin.html.j2",
|
return render_template("origin.html.j2",
|
||||||
section="origin",
|
section="origin",
|
||||||
origin=origin, form=form)
|
origin=origin, form=form)
|
||||||
|
@ -144,3 +158,74 @@ def origin_destroy(origin_id: int) -> ResponseReturnValue:
|
||||||
resource=origin,
|
resource=origin,
|
||||||
action="destroy"
|
action="destroy"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/country_remove/<origin_id>/<country_id>', methods=['GET', 'POST'])
|
||||||
|
def origin_country_remove(origin_id: int, country_id: int) -> ResponseReturnValue:
|
||||||
|
origin = Origin.query.filter(Origin.id == origin_id).first()
|
||||||
|
if origin is None:
|
||||||
|
return Response(render_template("error.html.j2",
|
||||||
|
section="origin",
|
||||||
|
header="404 Pool Not Found",
|
||||||
|
message="The requested origin could not be found."),
|
||||||
|
status=404)
|
||||||
|
country = Country.query.filter(Country.id == country_id).first()
|
||||||
|
if country is None:
|
||||||
|
return Response(render_template("error.html.j2",
|
||||||
|
section="origin",
|
||||||
|
header="404 Country Not Found",
|
||||||
|
message="The requested country could not be found."),
|
||||||
|
status=404)
|
||||||
|
if country not in origin.countries:
|
||||||
|
return Response(render_template("error.html.j2",
|
||||||
|
section="origin",
|
||||||
|
header="404 Country Not In Pool",
|
||||||
|
message="The requested country could not be found in the specified origin."),
|
||||||
|
status=404)
|
||||||
|
form = LifecycleForm()
|
||||||
|
if form.validate_on_submit():
|
||||||
|
origin.countries.remove(country)
|
||||||
|
try:
|
||||||
|
db.session.commit()
|
||||||
|
flash("Saved changes to origin.", "success")
|
||||||
|
return redirect(url_for("portal.origin.origin_edit", origin_id=origin.id))
|
||||||
|
except sqlalchemy.exc.SQLAlchemyError:
|
||||||
|
flash("An error occurred saving the changes to the origin.", "danger")
|
||||||
|
return render_template("lifecycle.html.j2",
|
||||||
|
header=f"Remove {country.country_name} from the {origin.domain_name} origin?",
|
||||||
|
message="Stop monitoring in this country.",
|
||||||
|
section="origin",
|
||||||
|
origin=origin, form=form)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/country_add/<origin_id>', methods=['GET', 'POST'])
|
||||||
|
def origin_country_add(origin_id: int) -> ResponseReturnValue:
|
||||||
|
origin = Origin.query.filter(Origin.id == origin_id).first()
|
||||||
|
if origin is None:
|
||||||
|
return Response(render_template("error.html.j2",
|
||||||
|
section="origin",
|
||||||
|
header="404 Origin Not Found",
|
||||||
|
message="The requested origin could not be found."),
|
||||||
|
status=404)
|
||||||
|
form = CountrySelectForm()
|
||||||
|
form.country.choices = [(x.id, f"{x.country_code} - {x.description}") for x in Country.query.all()]
|
||||||
|
if form.validate_on_submit():
|
||||||
|
country = Country.query.filter(Country.id == form.country.data).first()
|
||||||
|
if country is None:
|
||||||
|
return Response(render_template("error.html.j2",
|
||||||
|
section="origin",
|
||||||
|
header="404 Country Not Found",
|
||||||
|
message="The requested country could not be found."),
|
||||||
|
status=404)
|
||||||
|
origin.countries.append(country)
|
||||||
|
try:
|
||||||
|
db.session.commit()
|
||||||
|
flash("Saved changes to origin.", "success")
|
||||||
|
return redirect(url_for("portal.origin.origin_edit", origin_id=origin.id))
|
||||||
|
except sqlalchemy.exc.SQLAlchemyError:
|
||||||
|
flash("An error occurred saving the changes to the origin.", "danger")
|
||||||
|
return render_template("lifecycle.html.j2",
|
||||||
|
header=f"Add a country to {origin.domain_name}",
|
||||||
|
message="Enable monitoring from this country:",
|
||||||
|
section="origin",
|
||||||
|
origin=origin, form=form)
|
||||||
|
|
|
@ -87,6 +87,12 @@
|
||||||
{{ icon("cloud") }} Cloud Accounts
|
{{ icon("cloud") }} Cloud Accounts
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link{% if section == "country" %} active{% endif %}"
|
||||||
|
href="{{ url_for("portal.country.country_list") }}">
|
||||||
|
{{ icon("geo") }} Countries
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link{% if section == "pool" %} active{% endif %}"
|
<a class="nav-link{% if section == "pool" %} active{% endif %}"
|
||||||
href="{{ url_for("portal.pool.pool_list") }}">
|
href="{{ url_for("portal.pool.pool_list") }}">
|
||||||
|
|
17
app/portal/templates/country.html.j2
Normal file
17
app/portal/templates/country.html.j2
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
{% extends "base.html.j2" %}
|
||||||
|
{% from 'bootstrap5/form.html' import render_form %}
|
||||||
|
{% from "tables.html.j2" import origins_table %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<h1 class="h2 mt-3">Country</h1>
|
||||||
|
<h2 class="h3">{{ country.description }} {{ country.country_code | country_flag }}</h2>
|
||||||
|
|
||||||
|
<div style="border: 1px solid #666;" class="p-3">
|
||||||
|
{{ render_form(form) }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3 class="mt-3">Origins</h3>
|
||||||
|
{% if country.origins %}
|
||||||
|
{{ origins_table(country.origins) }}
|
||||||
|
{% endif %}
|
||||||
|
{% endblock %}
|
|
@ -69,6 +69,10 @@
|
||||||
viewBox="0 0 16 16">
|
viewBox="0 0 16 16">
|
||||||
<path d="M8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4.5a.5.5 0 0 0 .5-.5v-4h2v4a.5.5 0 0 0 .5.5H14a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146zM2.5 14V7.707l5.5-5.5 5.5 5.5V14H10v-4a.5.5 0 0 0-.5-.5h-3a.5.5 0 0 0-.5.5v4H2.5z"/>
|
<path d="M8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4.5a.5.5 0 0 0 .5-.5v-4h2v4a.5.5 0 0 0 .5.5H14a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146zM2.5 14V7.707l5.5-5.5 5.5 5.5V14H10v-4a.5.5 0 0 0-.5-.5h-3a.5.5 0 0 0-.5.5v4H2.5z"/>
|
||||||
</svg>
|
</svg>
|
||||||
|
{% elif i == "geo" %}
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-geo" viewBox="0 0 16 16">
|
||||||
|
<path fill-rule="evenodd" d="M8 1a3 3 0 1 0 0 6 3 3 0 0 0 0-6zM4 4a4 4 0 1 1 4.5 3.969V13.5a.5.5 0 0 1-1 0V7.97A4 4 0 0 1 4 3.999zm2.493 8.574a.5.5 0 0 1-.411.575c-.712.118-1.28.295-1.655.493a1.319 1.319 0 0 0-.37.265.301.301 0 0 0-.057.09V14l.002.008a.147.147 0 0 0 .016.033.617.617 0 0 0 .145.15c.165.13.435.27.813.395.751.25 1.82.414 3.024.414s2.273-.163 3.024-.414c.378-.126.648-.265.813-.395a.619.619 0 0 0 .146-.15.148.148 0 0 0 .015-.033L12 14v-.004a.301.301 0 0 0-.057-.09 1.318 1.318 0 0 0-.37-.264c-.376-.198-.943-.375-1.655-.493a.5.5 0 1 1 .164-.986c.77.127 1.452.328 1.957.594C12.5 13 13 13.4 13 14c0 .426-.26.752-.544.977-.29.228-.68.413-1.116.558-.878.293-2.059.465-3.34.465-1.281 0-2.462-.172-3.34-.465-.436-.145-.826-.33-1.116-.558C3.26 14.752 3 14.426 3 14c0-.599.5-1 .961-1.243.505-.266 1.187-.467 1.957-.594a.5.5 0 0 1 .575.411z"/>
|
||||||
|
</svg>
|
||||||
{% elif i == "life-preserver" %}
|
{% elif i == "life-preserver" %}
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-life-preserver"
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-life-preserver"
|
||||||
viewBox="0 0 16 16">
|
viewBox="0 0 16 16">
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
{% extends "base.html.j2" %}
|
{% extends "base.html.j2" %}
|
||||||
{% from "tables.html.j2" import alarms_table, automations_table, bridgeconfs_table, bridges_table, cloud_accounts_table,
|
{% from "tables.html.j2" import alarms_table, automations_table, bridgeconfs_table, bridges_table, cloud_accounts_table,
|
||||||
|
countries_table,
|
||||||
eotk_table, groups_table, instances_table, mirrorlists_table, origins_table, origin_onion_table, onions_table,
|
eotk_table, groups_table, instances_table, mirrorlists_table, origins_table, origin_onion_table, onions_table,
|
||||||
pools_table, proxies_table, static_table, webhook_table %}
|
pools_table, proxies_table, static_table, webhook_table %}
|
||||||
|
|
||||||
|
@ -21,6 +22,8 @@
|
||||||
{{ bridges_table(items) }}
|
{{ bridges_table(items) }}
|
||||||
{% elif item == "cloud account" %}
|
{% elif item == "cloud account" %}
|
||||||
{{ cloud_accounts_table(items) }}
|
{{ cloud_accounts_table(items) }}
|
||||||
|
{% elif item == "country" %}
|
||||||
|
{{ countries_table(items) }}
|
||||||
{% elif item == "eotk" %}
|
{% elif item == "eotk" %}
|
||||||
{{ eotk_table(items) }}
|
{{ eotk_table(items) }}
|
||||||
{% elif item == "group" %}
|
{% elif item == "group" %}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{% extends "base.html.j2" %}
|
{% extends "base.html.j2" %}
|
||||||
{% from 'bootstrap5/form.html' import render_form %}
|
{% from 'bootstrap5/form.html' import render_form %}
|
||||||
{% from "tables.html.j2" import alarms_table, bridges_table, proxies_table %}
|
{% from "tables.html.j2" import alarms_table, bridges_table, countries_table, proxies_table %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<h1 class="h2 mt-3">Origins</h1>
|
<h1 class="h2 mt-3">Origins</h1>
|
||||||
|
@ -14,6 +14,10 @@
|
||||||
{{ render_form(form) }}
|
{{ render_form(form) }}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<h3>Countries</h3>
|
||||||
|
<p><a href="{{ url_for("portal.origin.origin_country_add", origin_id=origin.id) }}" class="btn btn-sm btn-success">Add Country</a></p>
|
||||||
|
{{ countries_table(origin.countries, origin) }}
|
||||||
|
|
||||||
{% if origin.alarms %}
|
{% if origin.alarms %}
|
||||||
<h3>Alarms</h3>
|
<h3>Alarms</h3>
|
||||||
{{ alarms_table(origin.alarms) }}
|
{{ alarms_table(origin.alarms) }}
|
||||||
|
|
|
@ -51,6 +51,39 @@
|
||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
|
{% macro countries_table(countries, origin=None) %}
|
||||||
|
<div class="table-responsive">
|
||||||
|
<table class="table table-striped table-sm">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th scope="col">Country Code</th>
|
||||||
|
<th scope="col">Country</th>
|
||||||
|
<th scope="col">Risk Level</th>
|
||||||
|
<th scope="col">Origins</th>
|
||||||
|
<th scope="col">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for country in countries %}
|
||||||
|
<tr class="align-middle">
|
||||||
|
<td>{{ country.country_code }}</td>
|
||||||
|
<td>{{ country.description }} {{ country.country_code | country_flag }}</td>
|
||||||
|
<td>{{ country.risk_level | string }}</td>
|
||||||
|
<td>{{ country.origins | length }}</td>
|
||||||
|
<td>
|
||||||
|
<a href="{{ url_for("portal.country.country_edit", country_id=country.id) }}" class="btn btn-sm btn-primary">View/Edit</a>
|
||||||
|
{% if origin %}
|
||||||
|
<a href="{{ url_for("portal.origin.origin_country_remove", origin_id=origin.id, country_id=country.id) }}" class="btn btn-danger btn-sm">Remove</a>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
{% endmacro %}
|
||||||
|
|
||||||
|
|
||||||
{% macro instances_table(application, instances) %}
|
{% macro instances_table(application, instances) %}
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-striped table-sm">
|
<table class="table table-striped table-sm">
|
||||||
|
@ -110,6 +143,8 @@
|
||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
{% macro eotk_table(instances) %}
|
{% macro eotk_table(instances) %}
|
||||||
{{ instances_table("eotk", instances) }}
|
{{ instances_table("eotk", instances) }}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
@ -252,6 +287,7 @@
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">Name</th>
|
<th scope="col">Name</th>
|
||||||
<th scope="col">Description</th>
|
<th scope="col">Description</th>
|
||||||
|
<th scope="col">Risk Level</th>
|
||||||
<th scope="col">Auto-Rotation</th>
|
<th scope="col">Auto-Rotation</th>
|
||||||
<th scope="col">Smart Proxy</th>
|
<th scope="col">Smart Proxy</th>
|
||||||
<th scope="col">Assets Origin</th>
|
<th scope="col">Assets Origin</th>
|
||||||
|
@ -270,6 +306,7 @@
|
||||||
{{ origin.domain_name }}
|
{{ origin.domain_name }}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ origin.description }}</td>
|
<td>{{ origin.description }}</td>
|
||||||
|
<td>{{ origin.risk_level.values() | max | default("n/a") }}</td>
|
||||||
<td>{% if origin.auto_rotation %}✅{% else %}❌{% endif %}</td>
|
<td>{% if origin.auto_rotation %}✅{% else %}❌{% endif %}</td>
|
||||||
<td>{% if origin.smart %}✅{% else %}❌{% endif %}</td>
|
<td>{% if origin.smart %}✅{% else %}❌{% endif %}</td>
|
||||||
<td>{% if origin.assets %}✅{% else %}❌{% endif %}</td>
|
<td>{% if origin.assets %}✅{% else %}❌{% endif %}</td>
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import logging
|
import logging
|
||||||
|
from collections import defaultdict
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
@ -35,17 +36,26 @@ class BlockExternalAutomation(BlockMirrorAutomation):
|
||||||
|
|
||||||
def fetch(self) -> None:
|
def fetch(self) -> None:
|
||||||
user_agent = {'User-agent': 'BypassCensorship/1.0'}
|
user_agent = {'User-agent': 'BypassCensorship/1.0'}
|
||||||
if isinstance(app.config.get('EXTERNAL_CHECK_URL', []), list):
|
check_urls_config = app.config.get('EXTERNAL_CHECK_URL', [])
|
||||||
check_urls = app.config.get('EXTERNAL_CHECK_URL', [])
|
|
||||||
elif isinstance(app.config.get('EXTERNAL_CHECK_URL'), str):
|
if isinstance(check_urls_config, dict):
|
||||||
check_urls = [app.config['EXTERNAL_CHECK_URL']]
|
# Config is already a dictionary, use as is.
|
||||||
|
check_urls = check_urls_config
|
||||||
|
elif isinstance(check_urls_config, list):
|
||||||
|
# Convert list of strings to a dictionary with "external_N" keys.
|
||||||
|
check_urls = {f"external_{i}": url for i, url in enumerate(check_urls_config)}
|
||||||
|
elif isinstance(check_urls_config, str):
|
||||||
|
# Single string, convert to a dictionary with key "external".
|
||||||
|
check_urls = {"external": check_urls_config}
|
||||||
else:
|
else:
|
||||||
check_urls = []
|
# Fallback if the config item is neither dict, list, nor string.
|
||||||
for check_url in check_urls:
|
check_urls = {}
|
||||||
|
for source, check_url in check_urls.items():
|
||||||
if self._data is None:
|
if self._data is None:
|
||||||
self._data = []
|
self._data = defaultdict(list)
|
||||||
self._data.extend(requests.get(check_url, headers=user_agent, timeout=30).json())
|
self._data[source].extend(requests.get(check_url, headers=user_agent, timeout=30).json())
|
||||||
|
|
||||||
def parse(self) -> None:
|
def parse(self) -> None:
|
||||||
self.patterns.extend(["https://" + trim_http_https(pattern) for pattern in self._data])
|
for source, patterns in self._data.items():
|
||||||
|
self.patterns[source].extend(["https://" + trim_http_https(pattern) for pattern in patterns])
|
||||||
logging.debug("Found URLs: %s", self.patterns)
|
logging.debug("Found URLs: %s", self.patterns)
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
|
from collections import defaultdict
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
import logging
|
import logging
|
||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
import fnmatch
|
import fnmatch
|
||||||
from typing import Tuple, List, Any, Optional
|
from typing import Tuple, List, Any, Optional, Dict
|
||||||
|
|
||||||
from app.extensions import db
|
from app.extensions import db
|
||||||
from app.models.activity import Activity
|
from app.models.activity import Activity
|
||||||
|
@ -11,14 +12,14 @@ from app.terraform import BaseAutomation
|
||||||
|
|
||||||
|
|
||||||
class BlockMirrorAutomation(BaseAutomation):
|
class BlockMirrorAutomation(BaseAutomation):
|
||||||
patterns: List[str]
|
patterns: Dict[str, List[str]]
|
||||||
_data: Any
|
_data: Any
|
||||||
|
|
||||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||||
"""
|
"""
|
||||||
Constructor method.
|
Constructor method.
|
||||||
"""
|
"""
|
||||||
self.patterns = []
|
self.patterns = defaultdict(list)
|
||||||
self._data = None
|
self._data = None
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
@ -29,23 +30,25 @@ class BlockMirrorAutomation(BaseAutomation):
|
||||||
logging.debug("Parse complete")
|
logging.debug("Parse complete")
|
||||||
rotated = []
|
rotated = []
|
||||||
proxy_urls = list(filter(lambda u: u is not None, active_proxy_urls()))
|
proxy_urls = list(filter(lambda u: u is not None, active_proxy_urls()))
|
||||||
for pattern in self.patterns:
|
for source, patterns in self.patterns.items():
|
||||||
blocked_urls = fnmatch.filter(proxy_urls, pattern)
|
logging.debug("Processing blocked URLs from %s", source)
|
||||||
for blocked_url in blocked_urls:
|
for pattern in patterns:
|
||||||
if not (proxy := proxy_by_url(blocked_url)):
|
blocked_urls = fnmatch.filter(proxy_urls, pattern)
|
||||||
continue
|
for blocked_url in blocked_urls:
|
||||||
logging.debug("Found %s blocked", proxy.url)
|
if not (proxy := proxy_by_url(blocked_url)):
|
||||||
if not proxy.origin.auto_rotation:
|
continue
|
||||||
logging.debug("Proxy auto-rotation forbidden for origin")
|
logging.debug("Found %s blocked", proxy.url)
|
||||||
continue
|
if not proxy.origin.auto_rotation:
|
||||||
if proxy.added > datetime.utcnow() - timedelta(hours=3):
|
logging.debug("Proxy auto-rotation forbidden for origin")
|
||||||
logging.debug("Not rotating a proxy less than 3 hours old")
|
continue
|
||||||
continue
|
if proxy.added > datetime.utcnow() - timedelta(hours=3):
|
||||||
if proxy.deprecate(reason=self.short_name):
|
logging.debug("Not rotating a proxy less than 3 hours old")
|
||||||
logging.info("Rotated %s", proxy.url)
|
continue
|
||||||
rotated.append((proxy.url, proxy.origin.domain_name))
|
if proxy.deprecate(reason=f"block_{source}"):
|
||||||
else:
|
logging.info("Rotated %s", proxy.url)
|
||||||
logging.debug("Not rotating a proxy that is already deprecated")
|
rotated.append((proxy.url, proxy.origin.domain_name))
|
||||||
|
else:
|
||||||
|
logging.debug("Not rotating a proxy that is already deprecated")
|
||||||
if rotated:
|
if rotated:
|
||||||
activity = Activity(
|
activity = Activity(
|
||||||
activity_type="block",
|
activity_type="block",
|
||||||
|
|
|
@ -93,7 +93,7 @@ class BlockRoskomsvobodaAutomation(BlockMirrorAutomation):
|
||||||
for _event, element in lxml.etree.iterparse(BytesIO(self._data),
|
for _event, element in lxml.etree.iterparse(BytesIO(self._data),
|
||||||
resolve_entities=False):
|
resolve_entities=False):
|
||||||
if element.tag == "domain":
|
if element.tag == "domain":
|
||||||
self.patterns.append("https://" + element.text.strip())
|
self.patterns["roskomsvoboda"].append("https://" + element.text.strip())
|
||||||
except XMLSyntaxError:
|
except XMLSyntaxError:
|
||||||
activity = Activity(
|
activity = Activity(
|
||||||
activity_type="automation",
|
activity_type="automation",
|
||||||
|
|
1
migrations/countries.json
Normal file
1
migrations/countries.json
Normal file
File diff suppressed because one or more lines are too long
117
migrations/versions/bbec86de37c4_adds_geo_monitoring.py
Normal file
117
migrations/versions/bbec86de37c4_adds_geo_monitoring.py
Normal file
|
@ -0,0 +1,117 @@
|
||||||
|
import json
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy import table, column
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
revision = 'bbec86de37c4'
|
||||||
|
down_revision = '278bcfb487d3'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
op.create_table('country',
|
||||||
|
sa.Column('country_code', sa.String(length=2), nullable=False),
|
||||||
|
sa.Column('risk_level_override', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('description', sa.String(length=255), nullable=False),
|
||||||
|
sa.Column('added', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('updated', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('destroyed', sa.DateTime(), nullable=True),
|
||||||
|
sa.PrimaryKeyConstraint('id', name=op.f('pk_country'))
|
||||||
|
)
|
||||||
|
op.create_table('deprecation',
|
||||||
|
sa.Column('id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('resource_type', sa.String(length=50), nullable=True),
|
||||||
|
sa.Column('resource_id', sa.Integer(), nullable=True),
|
||||||
|
sa.Column('deprecated_at', sa.DateTime(), nullable=False),
|
||||||
|
sa.Column('reason', sa.String(), nullable=False),
|
||||||
|
sa.Column('meta', sa.JSON()),
|
||||||
|
sa.PrimaryKeyConstraint('id', name=op.f('pk_deprecation'))
|
||||||
|
)
|
||||||
|
op.create_table('country_origin',
|
||||||
|
sa.Column('country_id', sa.Integer(), nullable=False),
|
||||||
|
sa.Column('origin_id', sa.Integer(), nullable=False),
|
||||||
|
sa.ForeignKeyConstraint(['country_id'], ['country.id'],
|
||||||
|
name=op.f('fk_country_origin_country_id_country')),
|
||||||
|
sa.ForeignKeyConstraint(['origin_id'], ['origin.id'],
|
||||||
|
name=op.f('fk_country_origin_origin_id_origin')),
|
||||||
|
sa.PrimaryKeyConstraint('country_id', 'origin_id', name=op.f('pk_country_origin'))
|
||||||
|
)
|
||||||
|
with op.batch_alter_table('origin', schema=None) as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('risk_level_override', sa.Integer(), nullable=True))
|
||||||
|
|
||||||
|
countries = json.load(open("migrations/countries.json"))
|
||||||
|
|
||||||
|
country_table = table(
|
||||||
|
'country',
|
||||||
|
column('id', sa.Integer),
|
||||||
|
column('country_code', sa.String),
|
||||||
|
column('description', sa.String),
|
||||||
|
column('risk_level_override', sa.Integer),
|
||||||
|
column('added', sa.DateTime),
|
||||||
|
column('updated', sa.DateTime),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Iterate through each country and insert it using the 'country_table' definition
|
||||||
|
for country in countries:
|
||||||
|
op.execute(
|
||||||
|
country_table.insert().values(
|
||||||
|
country_code=country['Code'],
|
||||||
|
description=country['Name'],
|
||||||
|
risk_level_override=None, # Assuming risk level override is initially None
|
||||||
|
added=datetime.utcnow(),
|
||||||
|
updated=datetime.utcnow()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
deprecation_table = table('deprecation',
|
||||||
|
column('id', sa.Integer),
|
||||||
|
column('resource_type', sa.String),
|
||||||
|
column('resource_id', sa.Integer),
|
||||||
|
column('deprecated_at', sa.DateTime),
|
||||||
|
column('reason', sa.String)
|
||||||
|
)
|
||||||
|
|
||||||
|
bind = op.get_bind()
|
||||||
|
session = Session(bind=bind)
|
||||||
|
|
||||||
|
resource_tables = ['proxy', 'bridge']
|
||||||
|
|
||||||
|
for table_name in resource_tables:
|
||||||
|
# Query the existing deprecations
|
||||||
|
results = session.execute(
|
||||||
|
sa.select(
|
||||||
|
column('id'),
|
||||||
|
column('deprecated'),
|
||||||
|
column('deprecation_reason')
|
||||||
|
).select_from(table(table_name))
|
||||||
|
)
|
||||||
|
|
||||||
|
# Iterate over each row and create a corresponding entry in the Deprecation table
|
||||||
|
for id_, deprecated, reason in results:
|
||||||
|
if deprecated is not None: # Only migrate if there's a deprecation date
|
||||||
|
op.execute(
|
||||||
|
deprecation_table.insert().values(
|
||||||
|
resource_type=table_name.title(), # The class name is used, not the table name
|
||||||
|
resource_id=id_,
|
||||||
|
deprecated_at=deprecated,
|
||||||
|
reason=reason
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with op.batch_alter_table('origin', schema=None) as batch_op:
|
||||||
|
batch_op.drop_column('risk_level_override')
|
||||||
|
|
||||||
|
op.drop_table('country_origin')
|
||||||
|
op.drop_table('deprecation')
|
||||||
|
op.drop_table('country')
|
||||||
|
# ### end Alembic commands ###
|
Loading…
Add table
Add a link
Reference in a new issue