diff --git a/app/api/__init__.py b/app/api/__init__.py index 020f8c3..0b5653e 100644 --- a/app/api/__init__.py +++ b/app/api/__init__.py @@ -2,7 +2,7 @@ import base64 import binascii import logging import re -from typing import Optional, List, Callable, Any, Type, Dict, Union +from typing import Optional, List, Callable, Any, Type, Dict, Union, Literal from flask import Blueprint, request, jsonify, abort from flask.typing import ResponseReturnValue @@ -12,6 +12,7 @@ from werkzeug.exceptions import HTTPException from app.extensions import db from app.models.base import Group from app.models.mirrors import Origin, Proxy +from app.models.onions import Onion api = Blueprint('api', __name__) logger = logging.getLogger(__name__) @@ -70,6 +71,16 @@ def validate_marker(marker_str: str) -> int: abort(400, description="Marker must be a valid token.") +TLPMarkings = Union[ + Literal["default"], + Literal["clear"], + Literal["green"], + Literal["amber"], + Literal["amber+strict"], + Literal["red"], +] + + def list_resources( model: Type[Any], serialize_func: Callable[[Any], Dict[str, Any]], @@ -78,7 +89,8 @@ def list_resources( resource_name: str = 'ResourceList', max_items_param: str = 'MaxItems', marker_param: str = 'Marker', - max_allowed_items: int = 100 + max_allowed_items: int = 100, + protective_marking: TLPMarkings = 'default', ) -> ResponseReturnValue: try: marker = request.args.get(marker_param) @@ -108,6 +120,7 @@ def list_resources( "Quantity": len(items_list), "Items": items_list, "IsTruncated": is_truncated, + "ProtectiveMarking": protective_marking, } } @@ -128,7 +141,8 @@ def list_groups() -> ResponseReturnValue: model=Group, serialize_func=lambda group: group.to_dict(), resource_name='OriginGroupList', - max_allowed_items=MAX_ALLOWED_ITEMS + max_allowed_items=MAX_ALLOWED_ITEMS, + protective_marking='amber', ) @@ -157,7 +171,8 @@ def list_origins() -> ResponseReturnValue: serialize_func=lambda origin: origin.to_dict(), filters=filters, resource_name='OriginsList', - max_allowed_items=MAX_ALLOWED_ITEMS + max_allowed_items=MAX_ALLOWED_ITEMS, + protective_marking='amber', ) @@ -187,5 +202,36 @@ def list_mirrors() -> ResponseReturnValue: serialize_func=lambda proxy: proxy.to_dict(), filters=filters, resource_name='MirrorsList', - max_allowed_items=MAX_ALLOWED_ITEMS + max_allowed_items=MAX_ALLOWED_ITEMS, + protective_marking='amber', + ) + + +@api.route('/web/onion', methods=['GET']) +def list_onions() -> ResponseReturnValue: + domain_name_filter = request.args.get('DomainName') + group_id_filter = request.args.get('GroupId') + + filters: List[ListFilter] = [] + + if domain_name_filter: + if len(domain_name_filter) > MAX_DOMAIN_NAME_LENGTH: + abort(400, description=f"DomainName cannot exceed {MAX_DOMAIN_NAME_LENGTH} characters.") + if not DOMAIN_NAME_REGEX.match(domain_name_filter): + abort(400, description="DomainName contains invalid characters.") + filters.append(Onion.domain_name.ilike(f"%{domain_name_filter}%")) + + if group_id_filter: + try: + filters.append(Onion.group_id == int(group_id_filter)) + except ValueError: + abort(400, description="GroupId must be a valid integer.") + + return list_resources( + model=Onion, + serialize_func=lambda onion: onion.to_dict(), + filters=filters, + resource_name='OnionsList', + max_allowed_items=MAX_ALLOWED_ITEMS, + protective_marking='amber', ) diff --git a/app/models/onions.py b/app/models/onions.py index 6eda839..9fa628e 100644 --- a/app/models/onions.py +++ b/app/models/onions.py @@ -1,6 +1,6 @@ import base64 import hashlib -from typing import Optional +from typing import Optional, TypedDict from sqlalchemy.orm import Mapped, mapped_column, relationship @@ -10,6 +10,12 @@ from app.models import AbstractConfiguration, AbstractResource from app.models.base import Group +class OnionDict(TypedDict): + Id: int + DomainName: str + OnionName: str + + class Onion(AbstractConfiguration): @property def brn(self) -> BRN: @@ -49,6 +55,13 @@ class Onion(AbstractConfiguration): onion = base64.b32encode(result).decode("utf-8").strip("=") return onion.lower() + def to_dict(self) -> OnionDict: + return { + "Id": self.id, + "DomainName": self.domain_name, + "OnionName": self.onion_name, + } + class Eotk(AbstractResource): group_id: Mapped[int] = mapped_column(db.Integer(), db.ForeignKey("group.id"))