feat: geo risk scores
This commit is contained in:
parent
315dae7f06
commit
0e0d499428
17 changed files with 558 additions and 54 deletions
|
@ -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
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
|
||||
|
||||
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)
|
||||
|
|
|
@ -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") }}">
|
||||
|
|
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">
|
||||
<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">
|
||||
|
|
|
@ -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" %}
|
||||
|
|
|
@ -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) }}
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue