From e5ca63953d038cc2d591ce025c19cb2e421eec08 Mon Sep 17 00:00:00 2001 From: Yusur Princeps Date: Thu, 17 Jul 2025 21:33:11 +0200 Subject: [PATCH] add module .dorks and flask.harden() --- .gitignore | 1 + CHANGELOG.md | 1 + src/suou/__init__.py | 5 +++-- src/suou/dorks.py | 28 ++++++++++++++++++++++++++++ src/suou/exceptions.py | 6 ++++-- src/suou/flask.py | 20 ++++++++++++++++++-- src/suou/flask_restx.py | 2 +- src/suou/flask_sqlalchemy.py | 2 +- src/suou/itertools.py | 4 ++-- src/suou/lex.py | 4 +++- src/suou/peewee.py | 2 +- src/suou/sqlalchemy.py | 2 +- src/suou/validators.py | 2 +- 13 files changed, 65 insertions(+), 14 deletions(-) create mode 100644 src/suou/dorks.py diff --git a/.gitignore b/.gitignore index 2e2c6b7..7201aa6 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,4 @@ dist/ .err .vscode /run.sh +ROADMAP.md diff --git a/CHANGELOG.md b/CHANGELOG.md index b92eb48..ce764a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ + Added `ValueProperty`, abstract superclass for `ConfigProperty` + \[BREAKING] Changed the behavior of `makelist()`: now it's also a decorator, converting its return type to a list (revertable with `wrap=False`) + New module `lex` with functions `symbol_table()` and `lex()` — make tokenization more affordable ++ Add `dorks` module and `flask.harden()` + Added `addattr()` ## 0.3.6 diff --git a/src/suou/__init__.py b/src/suou/__init__.py index 94d793b..a3dfff1 100644 --- a/src/suou/__init__.py +++ b/src/suou/__init__.py @@ -26,6 +26,7 @@ from .classtools import Wanted, Incomplete from .itertools import makelist, kwargs_prefix, ltuple, rtuple, additem from .i18n import I18n, JsonI18n, TomlI18n from .snowflake import Snowflake, SnowflakeGen +from .lex import symbol_table, lex, ilex __version__ = "0.4.0-dev28" @@ -36,7 +37,7 @@ __all__ = ( 'SiqType', 'Snowflake', 'SnowflakeGen', 'StringCase', 'TomlI18n', 'Wanted', 'additem', 'b2048decode', 'b2048encode', 'b32ldecode', 'b32lencode', 'b64encode', 'b64decode', 'cb32encode', 'cb32decode', 'count_ones', - 'deprecated', 'join_bits', 'jsonencode', 'kwargs_prefix', 'ltuple', + 'deprecated', 'ilex', 'join_bits', 'jsonencode', 'kwargs_prefix', 'lex', 'ltuple', 'makelist', 'mask_shift', 'not_implemented', 'rtuple', 'split_bits', - 'ssv_list', 'want_bytes', 'want_str' + 'ssv_list', 'symbol_table', 'want_bytes', 'want_str' ) diff --git a/src/suou/dorks.py b/src/suou/dorks.py new file mode 100644 index 0000000..cf03ca5 --- /dev/null +++ b/src/suou/dorks.py @@ -0,0 +1,28 @@ +""" +Web app hardening and PT utilities. + +--- + +Copyright (c) 2025 Sakuragasaki46. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +See LICENSE for the specific language governing permissions and +limitations under the License. + +This software is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +""" + +SENSITIVE_ENDPOINTS = """ +/.git +/.gitignore +/node_modules +/wp-admin +/wp-login.php +/.ht +/package.json +/package-lock.json +/composer. +""".split() + diff --git a/src/suou/exceptions.py b/src/suou/exceptions.py index e6382c0..170125f 100644 --- a/src/suou/exceptions.py +++ b/src/suou/exceptions.py @@ -14,8 +14,6 @@ This software is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. """ -from .functools import deprecated - class MissingConfigError(LookupError): """ Config variable not found. @@ -42,3 +40,7 @@ class InconsistencyError(RuntimeError): """ This program is in a state which it's not supposed to be in. """ + +__all__ = ( + 'MissingConfigError', 'MissingConfigWarning', 'LexError', 'InconsistencyError' +) \ No newline at end of file diff --git a/src/suou/flask.py b/src/suou/flask.py index 97f1b16..a2ce4f9 100644 --- a/src/suou/flask.py +++ b/src/suou/flask.py @@ -15,9 +15,10 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. """ from typing import Any -from flask import Flask, current_app, g, request +from flask import Flask, abort, current_app, g, request from .i18n import I18n from .configparse import ConfigOptions +from .dorks import SENSITIVE_ENDPOINTS def add_context_from_config(app: Flask, config: ConfigOptions) -> Flask: @@ -66,6 +67,21 @@ def get_flask_conf(key: str, default = None, *, app: Flask | None = None) -> Any app = current_app return app.config.get(key, default) -__all__ = ('add_context_from_config', 'add_i18n', 'get_flask_conf') +## XXX UNTESTED! +def harden(app: Flask): + """ + Make common "dork" endpoints unavailable + """ + i = 1 + for ep in SENSITIVE_ENDPOINTS: + @app.route(f'{ep}', name=f'unavailable_{i}') + def unavailable(rest): + abort(403) + i += 1 + + return app + +# Optional dependency: do not import into __init__.py +__all__ = ('add_context_from_config', 'add_i18n', 'get_flask_conf', 'harden') diff --git a/src/suou/flask_restx.py b/src/suou/flask_restx.py index cef777e..bdddf04 100644 --- a/src/suou/flask_restx.py +++ b/src/suou/flask_restx.py @@ -74,5 +74,5 @@ class Api(_Api): super().__init__(*a, **ka) self.representations['application/json'] = output_json - +# Optional dependency: do not import into __init__.py __all__ = ('Api',) \ No newline at end of file diff --git a/src/suou/flask_sqlalchemy.py b/src/suou/flask_sqlalchemy.py index 5af6a8c..0704460 100644 --- a/src/suou/flask_sqlalchemy.py +++ b/src/suou/flask_sqlalchemy.py @@ -76,5 +76,5 @@ def require_auth(cls: type[DeclarativeBase], db: SQLAlchemy) -> Callable[Any, Ca return auth_required - +# Optional dependency: do not import into __init__.py __all__ = ('require_auth', ) diff --git a/src/suou/itertools.py b/src/suou/itertools.py index db1243c..abcfdfe 100644 --- a/src/suou/itertools.py +++ b/src/suou/itertools.py @@ -15,14 +15,14 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. ''' from functools import wraps -from typing import Any, Iterable, MutableMapping, TypeVar +from typing import Any, Callable, Iterable, MutableMapping, TypeVar import warnings from suou.classtools import MISSING _T = TypeVar('_T') -def makelist(l: Any, *, wrap: bool = True) -> list: +def makelist(l: Any, *, wrap: bool = True) -> list | Callable[Any, list]: ''' Make a list out of an iterable or a single value. diff --git a/src/suou/lex.py b/src/suou/lex.py index 086023f..15791c3 100644 --- a/src/suou/lex.py +++ b/src/suou/lex.py @@ -52,6 +52,7 @@ def symbol_table(*args: Iterable[tuple | TokenSym], whitespace: str | None = Non yield TokenSym('[' + re.escape(whitespace) + ']+', '', discard=True) +symbol_table: Callable[..., list] def ilex(text: str, table: Iterable[TokenSym], *, whitespace = False): """ @@ -80,5 +81,6 @@ def ilex(text: str, table: Iterable[TokenSym], *, whitespace = False): raise InconsistencyError i = mo.end(0) -lex = makelist(ilex) +lex: Callable[..., list] = makelist(ilex) +__all__ = ('symbol_table', 'lex', 'ilex') \ No newline at end of file diff --git a/src/suou/peewee.py b/src/suou/peewee.py index f5b9403..f1a3f1e 100644 --- a/src/suou/peewee.py +++ b/src/suou/peewee.py @@ -117,6 +117,6 @@ class SiqField(Field): def python_value(self, value: bytes) -> Siq: return Siq.from_bytes(value) - +# Optional dependency: do not import into __init__.py __all__ = ('connect_reconnect', 'RegexCharField', 'SiqField') diff --git a/src/suou/sqlalchemy.py b/src/suou/sqlalchemy.py index 249b104..edd1b02 100644 --- a/src/suou/sqlalchemy.py +++ b/src/suou/sqlalchemy.py @@ -295,7 +295,7 @@ def require_auth_base(cls: type[DeclarativeBase], *, src: AuthSrc, column: str | return wrapper return decorator - +# Optional dependency: do not import into __init__.py __all__ = ( 'IdType', 'id_column', 'entity_base', 'declarative_base', 'token_signer', 'match_column', 'match_constraint', 'author_pair', 'age_pair', 'require_auth_base', 'want_column' diff --git a/src/suou/validators.py b/src/suou/validators.py index b79882a..037d2b6 100644 --- a/src/suou/validators.py +++ b/src/suou/validators.py @@ -1,5 +1,5 @@ """ -Utilities for marshmallow, a schema-agnostic serializer/deserializer. +Miscellaneous validator closures. ---