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

@ -14,6 +14,7 @@ from app.models.bridges import Bridge
from app.models.mirrors import Origin, Proxy
from app.models.base import Group
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.automation import bp as automation
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(bridge, url_prefix="/bridge")
portal.register_blueprint(cloud, url_prefix="/cloud")
portal.register_blueprint(country, url_prefix="/country")
portal.register_blueprint(eotk, url_prefix="/eotk")
portal.register_blueprint(group, url_prefix="/group")
portal.register_blueprint(list_, url_prefix="/list")

74
app/portal/country.py Normal file
View 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)

View file

@ -3,17 +3,18 @@ from datetime import datetime
from typing import Optional, List
import requests
import sqlalchemy
from flask import flash, redirect, url_for, render_template, Response, Blueprint
from flask.typing import ResponseReturnValue
from flask_wtf import FlaskForm
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 app.extensions import db
from app.models.base import Group
from app.models.mirrors import Origin
from app.portal.util import response_404, view_lifecycle
from app.models.mirrors import Origin, Country
from app.portal.util import response_404, view_lifecycle, LifecycleForm
bp = Blueprint("origin", __name__)
@ -28,15 +29,22 @@ class NewOriginForm(FlaskForm): # type: ignore
submit = SubmitField('Save Changes')
class EditOriginForm(FlaskForm): # type: ignore
class EditOriginForm(FlaskForm): # type: ignore[misc]
description = StringField('Description', validators=[DataRequired()])
group = SelectField('Group', validators=[DataRequired()])
auto_rotate = BooleanField("Enable auto-rotation?")
smart_proxy = BooleanField("Requires smart proxy?")
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')
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:
session = requests.Session()
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,
auto_rotate=origin.auto_rotation,
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()]
if form.validate_on_submit():
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.smart = form.smart_proxy.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()
try:
db.session.commit()
flash("Saved changes to group.", "success")
flash(f"Saved changes for origin {origin.domain_name}.", "success")
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",
section="origin",
origin=origin, form=form)
@ -144,3 +158,74 @@ def origin_destroy(origin_id: int) -> ResponseReturnValue:
resource=origin,
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)

View file

@ -87,6 +87,12 @@
{{ icon("cloud") }} Cloud Accounts
</a>
</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">
<a class="nav-link{% if section == "pool" %} active{% endif %}"
href="{{ url_for("portal.pool.pool_list") }}">

View 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 %}

View file

@ -69,6 +69,10 @@
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"/>
</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" %}
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-life-preserver"
viewBox="0 0 16 16">

View file

@ -1,5 +1,6 @@
{% extends "base.html.j2" %}
{% 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,
pools_table, proxies_table, static_table, webhook_table %}
@ -21,6 +22,8 @@
{{ bridges_table(items) }}
{% elif item == "cloud account" %}
{{ cloud_accounts_table(items) }}
{% elif item == "country" %}
{{ countries_table(items) }}
{% elif item == "eotk" %}
{{ eotk_table(items) }}
{% elif item == "group" %}

View file

@ -1,6 +1,6 @@
{% extends "base.html.j2" %}
{% 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 %}
<h1 class="h2 mt-3">Origins</h1>
@ -14,6 +14,10 @@
{{ render_form(form) }}
</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 %}
<h3>Alarms</h3>
{{ alarms_table(origin.alarms) }}

View file

@ -51,6 +51,39 @@
</div>
{% 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) %}
<div class="table-responsive">
<table class="table table-striped table-sm">
@ -110,6 +143,8 @@
</div>
{% endmacro %}
{% macro eotk_table(instances) %}
{{ instances_table("eotk", instances) }}
{% endmacro %}
@ -252,6 +287,7 @@
<tr>
<th scope="col">Name</th>
<th scope="col">Description</th>
<th scope="col">Risk Level</th>
<th scope="col">Auto-Rotation</th>
<th scope="col">Smart Proxy</th>
<th scope="col">Assets Origin</th>
@ -270,6 +306,7 @@
{{ origin.domain_name }}
</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.smart %}✅{% else %}❌{% endif %}</td>
<td>{% if origin.assets %}✅{% else %}❌{% endif %}</td>