diff --git a/docs/genindex.html b/docs/genindex.html index 44c4b7b..edc3c39 100644 --- a/docs/genindex.html +++ b/docs/genindex.html @@ -50,6 +50,7 @@

Index

| G | H | I + | L | M | N | P @@ -66,16 +67,14 @@

A

  • allowed_origins (pyninja.models.Session attribute)
  • api_secret (pyninja.models.EnvConfig attribute) -
  • -
  • apikey (pyninja.models.EnvConfig attribute)
  • @@ -121,7 +120,7 @@

    E

    @@ -161,7 +162,7 @@

    G

    H

      @@ -173,7 +174,7 @@

      H

      I

        @@ -184,6 +185,18 @@

        I

      +

      L

      + + + +
      +

      M

        @@ -235,6 +248,8 @@

        P

      • parse_api_secret() (pyninja.models.EnvConfig class method)
      • Payload (class in pyninja.models) +
      • +
      • process_command() (in module pyninja.squire)
      • process_status() (in module pyninja.routers)
      • diff --git a/docs/index.html b/docs/index.html index 68b7ef9..9042246 100644 --- a/docs/index.html +++ b/docs/index.html @@ -79,33 +79,73 @@

        Welcome to PyNinja’s documentation!

        Authenticator

        -
        -async pyninja.auth.authenticator(token: HTTPBasicCredentials = Depends(HTTPBearer)) None
        -

        Validates the token if mentioned as a dependency.

        +
        +pyninja.auth.EPOCH()
        +
        + +
        +
        +async pyninja.auth.forbidden(request: Request) None
        +

        Validates if a request is part of the forbidden list.

        +
        +
        Parameters:
        +

        request – Reference to the FastAPI request object.

        +
        +
        Raises:
        +
          +
        • APIResponse

        • +
        • - 403 – If host address is forbidden.

        • +
        +
        +
        +
        + +
        +
        +async pyninja.auth.level_1(request: Request, apikey: HTTPAuthorizationCredentials) None
        +

        Validates the auth request using HTTPBearer.

        Parameters:
        -

        token – Takes the authorization header token as an argument.

        +
          +
        • request – Takes the authorization header token as an argument.

        • +
        • apikey – Basic APIKey required for all the routes.

        • +
        Raises:
        • APIResponse

        • - 401 – If authorization is invalid.

        • +
        • - 403 – If host address is forbidden.

        - -
        -

        Routers

        -
        -pyninja.routers.epoch()
        -
        +
        +async pyninja.auth.level_2(request: Request, apikey: HTTPAuthorizationCredentials, token: str) None
        +

        Validates the auth request using HTTPBearer and additionally a secure token.

        +
        +
        Parameters:
        +
          +
        • request – Takes the authorization header token as an argument.

        • +
        • apikey – Basic APIKey required for all the routes.

        • +
        • token – Additional token for critical requests.

        • +
        +
        +
        Raises:
        +
          +
        • APIResponse

        • +
        • - 401 – If authorization is invalid.

        • +
        • - 403 – If host address is forbidden.

        • +
        +
        +
        +
        -
        -async pyninja.routers.incrementer(attempt: int) int
        +
        +async pyninja.auth.incrementer(attempt: int) int

        Increments block time for a host address based on the number of failed attempts.

        Parameters:
        @@ -121,8 +161,8 @@

        Welcome to PyNinja’s documentation! -
        -async pyninja.routers.handle_auth_error(request: Request) None
        +
        +async pyninja.auth.handle_auth_error(request: Request) None

        Handle authentication errors from the filebrowser API.

        Parameters:
        @@ -131,9 +171,12 @@

        Welcome to PyNinja’s documentation! +

        Routers

        -async pyninja.routers.run_command(request: Request, payload: Payload, token: Optional[str] = Header(None))
        +async pyninja.routers.run_command(request: Request, payload: Payload, apikey: HTTPAuthorizationCredentials = Depends(HTTPBearer), token: Optional[str] = Header(None))

        API function to run a command on host machine.

        Args:

        @@ -148,7 +191,7 @@

        Welcome to PyNinja’s documentation!
        -async pyninja.routers.process_status(process_name: str)
        +async pyninja.routers.process_status(request: Request, process_name: str, apikey: HTTPAuthorizationCredentials = Depends(HTTPBearer))

        API function to monitor a process.

        Args:

        @@ -163,7 +206,7 @@

        Welcome to PyNinja’s documentation!
        -async pyninja.routers.service_status(service_name: str)
        +async pyninja.routers.service_status(request: Request, service_name: str, apikey: HTTPAuthorizationCredentials = Depends(HTTPBearer))

        API function to monitor a service.

        Args:

        @@ -212,10 +255,10 @@

        Monitors
        pyninja.process.get_process_status(process_name: str) Generator[Dict[str, int]]
        -

        Get process ID for a particular service.

        +

        Get process information by name.

        Parameters:
        -

        service_name (str) – Name of the service.

        +

        process_name – Name of the process.

        Yields:

        Generator[Dict[str, int]] – Yields the process metrics as a dictionary of key-value pairs.

        @@ -225,7 +268,7 @@

        Monitors
        -pyninja.process.get_performance(process_name: str, process: Process) Dict[str, int]
        +pyninja.process.get_performance(process: Process) Dict[str, int | float]

        Checks performance by monitoring CPU utilization, number of threads and open files.

        Parameters:
        @@ -599,6 +642,26 @@

        Models

        Squire

        +
        +
        +pyninja.squire.process_command(command: str, timeout: Union[int, float]) Dict[str, List[str]]
        +

        Process the requested command.

        +
        +
        Parameters:
        +
          +
        • command – Command as string.

        • +
        • timeout – Timeout for the command.

        • +
        +
        +
        Returns:
        +

        Returns the result with stdout and stderr as key-value pairs.

        +
        +
        Return type:
        +

        Dict[str, List[str]]

        +
        +
        +
        +
        pyninja.squire.env_loader(filename: str | os.PathLike) EnvConfig
        diff --git a/docs/objects.inv b/docs/objects.inv index 0059079..1e72b12 100644 Binary files a/docs/objects.inv and b/docs/objects.inv differ diff --git a/docs/searchindex.js b/docs/searchindex.js index c296e96..8795b6f 100644 --- a/docs/searchindex.js +++ b/docs/searchindex.js @@ -1 +1 @@ -Search.setIndex({"docnames": ["README", "index"], "filenames": ["README.md", "index.rst"], "titles": ["PyNinja", "Welcome to PyNinja\u2019s documentation!"], "terms": {"lightweight": 0, "o": [0, 1], "agnost": 0, "servic": 0, "monitor": 0, "api": [0, 1], "platform": 0, "support": 0, "deploy": 0, "recommend": 0, "instal": 0, "python": 0, "3": 0, "10": [0, 1], "11": 0, "us": [0, 1], "dedic": 0, "virtual": 0, "m": 0, "pip": 0, "initi": 0, "id": [0, 1], "import": 0, "__name__": 0, "__main__": 0, "start": [0, 1], "cli": 0, "help": 0, "usag": 0, "instruct": 0, "sourc": 0, "from": [0, 1], "an": [0, 1], "env": [0, 1], "file": [0, 1], "By": 0, "default": 0, "look": 0, "current": 0, "work": 0, "directori": 0, "ninja": 0, "_": 0, "host": [0, 1], "hostnam": 0, "server": [0, 1], "port": 0, "number": [0, 1], "worker": [0, 1], "uvicorn": [0, 1], "remot": 0, "execut": 0, "boolean": 0, "flag": 0, "enabl": 0, "secret": [0, 1], "access": 0, "kei": [0, 1], "run": [0, 1], "command": [0, 1], "apikei": [0, 1], "authent": 0, "can": 0, "extrem": 0, "riski": 0, "major": 0, "secur": 0, "threat": 0, "so": 0, "caution": 0, "set": [0, 1], "strong": [0, 1], "valu": [0, 1], "log": 0, "ini": 0, "configur": [0, 1], "custom": [0, 1], "just": 0, "place": 0, "refer": [0, 1], "sampl": 0, "exampl": 0, "docstr": 0, "format": 0, "googl": 0, "style": 0, "convent": 0, "pep": 0, "8": 0, "isort": 0, "requir": [0, 1], "gitvers": 0, "revers": 0, "f": 0, "release_not": 0, "rst": 0, "t": 0, "pre": 0, "commit": 0, "ensur": 0, "pytest": 0, "gener": [0, 1], "valid": [0, 1], "hyperlink": 0, "all": [0, 1], "markdown": 0, "includ": 0, "wiki": 0, "page": [0, 1], "sphinx": 0, "5": 0, "1": [0, 1], "recommonmark": 0, "http": 0, "org": 0, "project": 0, "thevickypedia": 0, "github": 0, "io": 0, "vignesh": 0, "rao": 0, "under": 0, "mit": 0, "kick": 1, "off": 1, "environ": 1, "variabl": 1, "code": 1, "standard": 1, "releas": 1, "note": 1, "lint": 1, "pypi": 1, "packag": 1, "runbook": 1, "licens": 1, "copyright": 1, "kwarg": 1, "none": 1, "starter": 1, "function": 1, "which": 1, "trigger": 1, "keyword": 1, "argument": 1, "env_fil": 1, "filepath": 1, "async": 1, "auth": 1, "token": 1, "httpbasiccredenti": 1, "depend": 1, "httpbearer": 1, "mention": 1, "paramet": 1, "take": 1, "author": 1, "header": 1, "rais": 1, "apirespons": 1, "401": 1, "If": 1, "i": 1, "invalid": 1, "epoch": 1, "increment": 1, "attempt": 1, "int": 1, "block": 1, "time": 1, "address": 1, "base": 1, "fail": 1, "return": 1, "appropri": 1, "minut": 1, "type": 1, "handle_auth_error": 1, "request": 1, "handl": 1, "error": 1, "filebrows": 1, "The": 1, "incom": 1, "object": 1, "run_command": 1, "payload": 1, "option": 1, "str": 1, "machin": 1, "arg": 1, "receiv": 1, "bodi": 1, "httpstatu": 1, "statu": 1, "detail": 1, "respons": 1, "process_statu": 1, "process_nam": 1, "name": 1, "check": 1, "service_statu": 1, "service_nam": 1, "doc": 1, "redirectrespons": 1, "redirect": 1, "user": 1, "get_all_rout": 1, "list": 1, "apirout": 1, "get": 1, "rout": 1, "ad": 1, "get_process_statu": 1, "dict": 1, "particular": 1, "yield": 1, "metric": 1, "dictionari": 1, "pair": 1, "get_perform": 1, "perform": 1, "cpu": 1, "util": 1, "thread": 1, "open": 1, "get_service_statu": 1, "servicestatu": 1, "instanc": 1, "get_record": 1, "until": 1, "when": 1, "should": 1, "put_record": 1, "block_until": 1, "insert": 1, "remove_record": 1, "delet": 1, "record": 1, "relat": 1, "class": 1, "rate_limit": 1, "rp": 1, "implement": 1, "init": 1, "call": 1, "exce": 1, "rate": 1, "limit": 1, "given": 1, "identifi": 1, "429": 1, "too": 1, "mani": 1, "status_cod": 1, "ani": 1, "httpexcept": 1, "fastapi": 1, "wrap": 1, "unsupportedo": 1, "unsupport": 1, "basemodel": 1, "input": 1, "data": 1, "timeout": 1, "union": 1, "float": 1, "load": 1, "descript": 1, "session": 1, "store": 1, "inform": 1, "auth_count": 1, "forbid": 1, "info": 1, "allowed_origin": 1, "max_request": 1, "second": 1, "envconfig": 1, "ninja_host": 1, "ninja_port": 1, "remote_execut": 1, "bool": 1, "api_secret": 1, "classmethod": 1, "parse_api_secret": 1, "pars": 1, "complex": 1, "from_env_fil": 1, "path": 1, "creat": 1, "config": 1, "extra": 1, "ignor": 1, "hide_input_in_error": 1, "true": 1, "complexity_check": 1, "verifi": 1, "strength": 1, "A": 1, "consid": 1, "least": 1, "ha": 1, "32": 1, "charact": 1, "digit": 1, "symbol": 1, "uppercas": 1, "letter": 1, "lowercas": 1, "assertionerror": 1, "abov": 1, "condit": 1, "match": 1, "datastor": 1, "connect": 1, "sqlite3": 1, "create_t": 1, "table_nam": 1, "column": 1, "tupl": 1, "env_load": 1, "filenam": 1, "pathlik": 1, "filetyp": 1, "where": 1, "var": 1, "have": 1, "index": 1, "modul": 1, "search": 1}, "objects": {"pyninja": [[1, 0, 0, "-", "auth"], [1, 0, 0, "-", "database"], [1, 0, 0, "-", "exceptions"], [1, 0, 0, "-", "main"], [1, 0, 0, "-", "models"], [1, 0, 0, "-", "process"], [1, 0, 0, "-", "rate_limit"], [1, 0, 0, "-", "routers"], [1, 0, 0, "-", "service"], [1, 0, 0, "-", "squire"]], "pyninja.auth": [[1, 1, 1, "", "authenticator"]], "pyninja.database": [[1, 1, 1, "", "get_record"], [1, 1, 1, "", "put_record"], [1, 1, 1, "", "remove_record"]], "pyninja.exceptions": [[1, 2, 1, "", "APIResponse"], [1, 2, 1, "", "UnSupportedOS"]], "pyninja.main": [[1, 1, 1, "", "start"]], "pyninja.models": [[1, 3, 1, "", "Database"], [1, 3, 1, "", "EnvConfig"], [1, 3, 1, "", "Payload"], [1, 3, 1, "", "RateLimit"], [1, 3, 1, "", "ServiceStatus"], [1, 3, 1, "", "Session"], [1, 1, 1, "", "complexity_checker"]], "pyninja.models.Database": [[1, 4, 1, "", "create_table"]], "pyninja.models.EnvConfig": [[1, 3, 1, "", "Config"], [1, 5, 1, "", "api_secret"], [1, 5, 1, "", "apikey"], [1, 5, 1, "", "database"], [1, 4, 1, "", "from_env_file"], [1, 5, 1, "", "ninja_host"], [1, 5, 1, "", "ninja_port"], [1, 4, 1, "", "parse_api_secret"], [1, 5, 1, "", "rate_limit"], [1, 5, 1, "", "remote_execution"], [1, 5, 1, "", "workers"]], "pyninja.models.EnvConfig.Config": [[1, 5, 1, "", "extra"], [1, 5, 1, "", "hide_input_in_errors"]], "pyninja.models.Payload": [[1, 5, 1, "", "command"], [1, 5, 1, "", "timeout"]], "pyninja.models.RateLimit": [[1, 5, 1, "", "max_requests"], [1, 5, 1, "", "seconds"]], "pyninja.models.ServiceStatus": [[1, 5, 1, "", "description"], [1, 5, 1, "", "status_code"]], "pyninja.models.Session": [[1, 5, 1, "", "allowed_origins"], [1, 5, 1, "", "auth_counter"], [1, 5, 1, "", "forbid"], [1, 5, 1, "", "info"], [1, 5, 1, "", "rps"]], "pyninja.process": [[1, 1, 1, "", "get_performance"], [1, 1, 1, "", "get_process_status"]], "pyninja.rate_limit": [[1, 3, 1, "", "RateLimiter"]], "pyninja.rate_limit.RateLimiter": [[1, 4, 1, "", "init"]], "pyninja.routers": [[1, 1, 1, "", "docs"], [1, 1, 1, "", "epoch"], [1, 1, 1, "", "get_all_routes"], [1, 1, 1, "", "handle_auth_error"], [1, 1, 1, "", "incrementer"], [1, 1, 1, "", "process_status"], [1, 1, 1, "", "run_command"], [1, 1, 1, "", "service_status"]], "pyninja.service": [[1, 1, 1, "", "get_service_status"]], "pyninja.squire": [[1, 1, 1, "", "env_loader"]]}, "objtypes": {"0": "py:module", "1": "py:function", "2": "py:exception", "3": "py:class", "4": "py:method", "5": "py:attribute"}, "objnames": {"0": ["py", "module", "Python module"], "1": ["py", "function", "Python function"], "2": ["py", "exception", "Python exception"], "3": ["py", "class", "Python class"], "4": ["py", "method", "Python method"], "5": ["py", "attribute", "Python attribute"]}, "titleterms": {"pyninja": [0, 1], "kick": 0, "off": 0, "environ": 0, "variabl": 0, "code": 0, "standard": 0, "releas": 0, "note": 0, "lint": 0, "pypi": 0, "packag": 0, "runbook": 0, "licens": 0, "copyright": 0, "welcom": 1, "": 1, "document": 1, "content": 1, "main": 1, "authent": 1, "router": 1, "monitor": 1, "process": 1, "servic": 1, "databas": 1, "ratelimit": 1, "except": 1, "model": 1, "squir": 1, "indic": 1, "tabl": 1}, "envversion": {"sphinx.domains.c": 2, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 6, "sphinx.domains.index": 1, "sphinx.domains.javascript": 2, "sphinx.domains.math": 2, "sphinx.domains.python": 3, "sphinx.domains.rst": 2, "sphinx.domains.std": 2, "sphinx": 56}}) \ No newline at end of file +Search.setIndex({"docnames": ["README", "index"], "filenames": ["README.md", "index.rst"], "titles": ["PyNinja", "Welcome to PyNinja\u2019s documentation!"], "terms": {"lightweight": 0, "o": [0, 1], "agnost": 0, "servic": 0, "monitor": 0, "api": [0, 1], "platform": 0, "support": 0, "deploy": 0, "recommend": 0, "instal": 0, "python": 0, "3": 0, "10": [0, 1], "11": 0, "us": [0, 1], "dedic": 0, "virtual": 0, "m": 0, "pip": 0, "initi": 0, "id": 0, "import": 0, "__name__": 0, "__main__": 0, "start": [0, 1], "cli": 0, "help": 0, "usag": 0, "instruct": 0, "sourc": 0, "from": [0, 1], "an": [0, 1], "env": [0, 1], "file": [0, 1], "By": 0, "default": 0, "look": 0, "current": 0, "work": 0, "directori": 0, "ninja": 0, "_": 0, "host": [0, 1], "hostnam": 0, "server": [0, 1], "port": 0, "number": [0, 1], "worker": [0, 1], "uvicorn": [0, 1], "remot": 0, "execut": 0, "boolean": 0, "flag": 0, "enabl": 0, "secret": [0, 1], "access": 0, "kei": [0, 1], "run": [0, 1], "command": [0, 1], "apikei": [0, 1], "authent": 0, "can": 0, "extrem": 0, "riski": 0, "major": 0, "secur": [0, 1], "threat": 0, "so": 0, "caution": 0, "set": [0, 1], "strong": [0, 1], "valu": [0, 1], "log": 0, "ini": 0, "configur": [0, 1], "custom": [0, 1], "just": 0, "place": 0, "refer": [0, 1], "sampl": 0, "exampl": 0, "docstr": 0, "format": 0, "googl": 0, "style": 0, "convent": 0, "pep": 0, "8": 0, "isort": 0, "requir": [0, 1], "gitvers": 0, "revers": 0, "f": 0, "release_not": 0, "rst": 0, "t": 0, "pre": 0, "commit": 0, "ensur": 0, "pytest": 0, "gener": [0, 1], "valid": [0, 1], "hyperlink": 0, "all": [0, 1], "markdown": 0, "includ": 0, "wiki": 0, "page": [0, 1], "sphinx": 0, "5": 0, "1": [0, 1], "recommonmark": 0, "http": 0, "org": 0, "project": 0, "thevickypedia": 0, "github": 0, "io": 0, "vignesh": 0, "rao": 0, "under": 0, "mit": 0, "kick": 1, "off": 1, "environ": 1, "variabl": 1, "code": 1, "standard": 1, "releas": 1, "note": 1, "lint": 1, "pypi": 1, "packag": 1, "runbook": 1, "licens": 1, "copyright": 1, "kwarg": 1, "none": 1, "starter": 1, "function": 1, "which": 1, "trigger": 1, "keyword": 1, "argument": 1, "env_fil": 1, "filepath": 1, "auth": 1, "epoch": 1, "async": 1, "forbidden": 1, "request": 1, "i": 1, "part": 1, "list": 1, "paramet": 1, "fastapi": 1, "object": 1, "rais": 1, "apirespons": 1, "403": 1, "If": 1, "address": 1, "level_1": 1, "httpauthorizationcredenti": 1, "httpbearer": 1, "take": 1, "author": 1, "header": 1, "token": 1, "basic": 1, "rout": 1, "401": 1, "invalid": 1, "level_2": 1, "str": 1, "addition": 1, "addit": 1, "critic": 1, "increment": 1, "attempt": 1, "int": 1, "block": 1, "time": 1, "base": 1, "fail": 1, "return": 1, "appropri": 1, "minut": 1, "type": 1, "handle_auth_error": 1, "handl": 1, "error": 1, "filebrows": 1, "The": 1, "incom": 1, "run_command": 1, "payload": 1, "depend": 1, "option": 1, "machin": 1, "arg": 1, "receiv": 1, "bodi": 1, "httpstatu": 1, "statu": 1, "detail": 1, "respons": 1, "process_statu": 1, "process_nam": 1, "name": 1, "check": 1, "service_statu": 1, "service_nam": 1, "doc": 1, "redirectrespons": 1, "redirect": 1, "user": 1, "get_all_rout": 1, "apirout": 1, "get": 1, "ad": 1, "get_process_statu": 1, "dict": 1, "inform": 1, "yield": 1, "metric": 1, "dictionari": 1, "pair": 1, "get_perform": 1, "float": 1, "perform": 1, "cpu": 1, "util": 1, "thread": 1, "open": 1, "get_service_statu": 1, "servicestatu": 1, "instanc": 1, "get_record": 1, "particular": 1, "until": 1, "when": 1, "should": 1, "put_record": 1, "block_until": 1, "insert": 1, "remove_record": 1, "delet": 1, "record": 1, "relat": 1, "class": 1, "rate_limit": 1, "rp": 1, "implement": 1, "init": 1, "call": 1, "exce": 1, "rate": 1, "limit": 1, "given": 1, "identifi": 1, "429": 1, "too": 1, "mani": 1, "status_cod": 1, "ani": 1, "httpexcept": 1, "wrap": 1, "unsupportedo": 1, "unsupport": 1, "basemodel": 1, "input": 1, "data": 1, "timeout": 1, "union": 1, "load": 1, "descript": 1, "session": 1, "store": 1, "auth_count": 1, "forbid": 1, "info": 1, "allowed_origin": 1, "max_request": 1, "second": 1, "envconfig": 1, "ninja_host": 1, "ninja_port": 1, "remote_execut": 1, "bool": 1, "api_secret": 1, "classmethod": 1, "parse_api_secret": 1, "pars": 1, "complex": 1, "from_env_fil": 1, "path": 1, "creat": 1, "config": 1, "extra": 1, "ignor": 1, "hide_input_in_error": 1, "true": 1, "complexity_check": 1, "verifi": 1, "strength": 1, "A": 1, "consid": 1, "least": 1, "ha": 1, "32": 1, "charact": 1, "digit": 1, "symbol": 1, "uppercas": 1, "letter": 1, "lowercas": 1, "assertionerror": 1, "abov": 1, "condit": 1, "match": 1, "datastor": 1, "connect": 1, "sqlite3": 1, "create_t": 1, "table_nam": 1, "column": 1, "tupl": 1, "process_command": 1, "string": 1, "result": 1, "stdout": 1, "stderr": 1, "env_load": 1, "filenam": 1, "pathlik": 1, "filetyp": 1, "where": 1, "var": 1, "have": 1, "index": 1, "modul": 1, "search": 1}, "objects": {"pyninja": [[1, 0, 0, "-", "auth"], [1, 0, 0, "-", "database"], [1, 0, 0, "-", "exceptions"], [1, 0, 0, "-", "main"], [1, 0, 0, "-", "models"], [1, 0, 0, "-", "process"], [1, 0, 0, "-", "rate_limit"], [1, 0, 0, "-", "routers"], [1, 0, 0, "-", "service"], [1, 0, 0, "-", "squire"]], "pyninja.auth": [[1, 1, 1, "", "EPOCH"], [1, 1, 1, "", "forbidden"], [1, 1, 1, "", "handle_auth_error"], [1, 1, 1, "", "incrementer"], [1, 1, 1, "", "level_1"], [1, 1, 1, "", "level_2"]], "pyninja.database": [[1, 1, 1, "", "get_record"], [1, 1, 1, "", "put_record"], [1, 1, 1, "", "remove_record"]], "pyninja.exceptions": [[1, 2, 1, "", "APIResponse"], [1, 2, 1, "", "UnSupportedOS"]], "pyninja.main": [[1, 1, 1, "", "start"]], "pyninja.models": [[1, 3, 1, "", "Database"], [1, 3, 1, "", "EnvConfig"], [1, 3, 1, "", "Payload"], [1, 3, 1, "", "RateLimit"], [1, 3, 1, "", "ServiceStatus"], [1, 3, 1, "", "Session"], [1, 1, 1, "", "complexity_checker"]], "pyninja.models.Database": [[1, 4, 1, "", "create_table"]], "pyninja.models.EnvConfig": [[1, 3, 1, "", "Config"], [1, 5, 1, "", "api_secret"], [1, 5, 1, "", "apikey"], [1, 5, 1, "", "database"], [1, 4, 1, "", "from_env_file"], [1, 5, 1, "", "ninja_host"], [1, 5, 1, "", "ninja_port"], [1, 4, 1, "", "parse_api_secret"], [1, 5, 1, "", "rate_limit"], [1, 5, 1, "", "remote_execution"], [1, 5, 1, "", "workers"]], "pyninja.models.EnvConfig.Config": [[1, 5, 1, "", "extra"], [1, 5, 1, "", "hide_input_in_errors"]], "pyninja.models.Payload": [[1, 5, 1, "", "command"], [1, 5, 1, "", "timeout"]], "pyninja.models.RateLimit": [[1, 5, 1, "", "max_requests"], [1, 5, 1, "", "seconds"]], "pyninja.models.ServiceStatus": [[1, 5, 1, "", "description"], [1, 5, 1, "", "status_code"]], "pyninja.models.Session": [[1, 5, 1, "", "allowed_origins"], [1, 5, 1, "", "auth_counter"], [1, 5, 1, "", "forbid"], [1, 5, 1, "", "info"], [1, 5, 1, "", "rps"]], "pyninja.process": [[1, 1, 1, "", "get_performance"], [1, 1, 1, "", "get_process_status"]], "pyninja.rate_limit": [[1, 3, 1, "", "RateLimiter"]], "pyninja.rate_limit.RateLimiter": [[1, 4, 1, "", "init"]], "pyninja.routers": [[1, 1, 1, "", "docs"], [1, 1, 1, "", "get_all_routes"], [1, 1, 1, "", "process_status"], [1, 1, 1, "", "run_command"], [1, 1, 1, "", "service_status"]], "pyninja.service": [[1, 1, 1, "", "get_service_status"]], "pyninja.squire": [[1, 1, 1, "", "env_loader"], [1, 1, 1, "", "process_command"]]}, "objtypes": {"0": "py:module", "1": "py:function", "2": "py:exception", "3": "py:class", "4": "py:method", "5": "py:attribute"}, "objnames": {"0": ["py", "module", "Python module"], "1": ["py", "function", "Python function"], "2": ["py", "exception", "Python exception"], "3": ["py", "class", "Python class"], "4": ["py", "method", "Python method"], "5": ["py", "attribute", "Python attribute"]}, "titleterms": {"pyninja": [0, 1], "kick": 0, "off": 0, "environ": 0, "variabl": 0, "code": 0, "standard": 0, "releas": 0, "note": 0, "lint": 0, "pypi": 0, "packag": 0, "runbook": 0, "licens": 0, "copyright": 0, "welcom": 1, "": 1, "document": 1, "content": 1, "main": 1, "authent": 1, "router": 1, "monitor": 1, "process": 1, "servic": 1, "databas": 1, "ratelimit": 1, "except": 1, "model": 1, "squir": 1, "indic": 1, "tabl": 1}, "envversion": {"sphinx.domains.c": 2, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 6, "sphinx.domains.index": 1, "sphinx.domains.javascript": 2, "sphinx.domains.math": 2, "sphinx.domains.python": 3, "sphinx.domains.rst": 2, "sphinx.domains.std": 2, "sphinx": 56}}) \ No newline at end of file diff --git a/pyninja/auth.py b/pyninja/auth.py index b0eb884..61640c8 100644 --- a/pyninja/auth.py +++ b/pyninja/auth.py @@ -4,8 +4,8 @@ from datetime import datetime from http import HTTPStatus -from fastapi import Depends, Request -from fastapi.security import HTTPBasicCredentials, HTTPBearer +from fastapi import Request +from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer from pyninja import database, exceptions, models @@ -14,26 +14,96 @@ SECURITY = HTTPBearer() -async def authenticator(token: HTTPBasicCredentials = Depends(SECURITY)) -> None: - """Validates the token if mentioned as a dependency. +async def forbidden(request: Request) -> None: + """Validates if a request is part of the forbidden list. Args: - token: Takes the authorization header token as an argument. + request: Reference to the FastAPI request object. + + Raises: + APIResponse: + - 403: If host address is forbidden. + """ + # placeholder list, to avoid a DB search for every request + if request.client.host in models.session.forbid: + # Get timestamp until which the host has to be forbidden + timestamp = database.get_record(request.client.host) + if timestamp and timestamp > EPOCH(): + LOGGER.warning( + "%s is forbidden until %s due to repeated login failures", + request.client.host, + datetime.fromtimestamp(timestamp).strftime("%c"), + ) + raise exceptions.APIResponse( + status_code=HTTPStatus.FORBIDDEN.value, + detail=f"{request.client.host!r} is not allowed", + ) + + +async def level_1(request: Request, apikey: HTTPAuthorizationCredentials) -> None: + """Validates the auth request using HTTPBearer. + + Args: + request: Takes the authorization header token as an argument. + apikey: Basic APIKey required for all the routes. Raises: APIResponse: - 401: If authorization is invalid. + - 403: If host address is forbidden. """ - auth = token.model_dump().get("credentials", "") - if auth.startswith("\\"): - auth = bytes(auth, "utf-8").decode(encoding="unicode_escape") + await forbidden(request) + if apikey.credentials.startswith("\\"): + auth = bytes(apikey.credentials, "utf-8").decode(encoding="unicode_escape") + else: + auth = apikey.credentials if secrets.compare_digest(auth, models.env.apikey): + LOGGER.info( + "Connection received from client-host: %s, host-header: %s, x-fwd-host: %s", + request.client.host, + request.headers.get("host"), + request.headers.get("x-forwarded-host"), + ) + if user_agent := request.headers.get("user-agent"): + LOGGER.info("User agent: %s", user_agent) return + # Adds host address to the forbidden set + await handle_auth_error(request) raise exceptions.APIResponse( status_code=HTTPStatus.UNAUTHORIZED.real, detail=HTTPStatus.UNAUTHORIZED.phrase ) +async def level_2( + request: Request, apikey: HTTPAuthorizationCredentials, token: str +) -> None: + """Validates the auth request using HTTPBearer and additionally a secure token. + + Args: + request: Takes the authorization header token as an argument. + apikey: Basic APIKey required for all the routes. + token: Additional token for critical requests. + + Raises: + APIResponse: + - 401: If authorization is invalid. + - 403: If host address is forbidden. + """ + await level_1(request, apikey) + if not all((models.env.remote_execution, models.env.api_secret)): + raise exceptions.APIResponse( + status_code=HTTPStatus.NOT_IMPLEMENTED.real, + detail="Remote execution has been disabled on the server.", + ) + if token and secrets.compare_digest(token, models.env.api_secret): + return + await handle_auth_error(request) + raise exceptions.APIResponse( + status_code=HTTPStatus.UNAUTHORIZED.real, + detail=HTTPStatus.UNAUTHORIZED.phrase, + ) + + async def incrementer(attempt: int) -> int: """Increments block time for a host address based on the number of failed attempts. diff --git a/pyninja/main.py b/pyninja/main.py index 845b6a9..b998488 100644 --- a/pyninja/main.py +++ b/pyninja/main.py @@ -7,7 +7,7 @@ import pyninja from pyninja import models, routers, squire -LOGGER = logging.getLogger(__name__) +LOGGER = logging.getLogger("uvicorn.error") def start(**kwargs) -> None: diff --git a/pyninja/process.py b/pyninja/process.py index 1e1c9f3..6a9671d 100644 --- a/pyninja/process.py +++ b/pyninja/process.py @@ -8,22 +8,32 @@ def get_process_status(process_name: str) -> Generator[Dict[str, int]]: - """Get process ID for a particular service. + """Get process information by name. Args: - service_name (str): Name of the service. + process_name: Name of the process. Yields: Generator[Dict[str, int]]: Yields the process metrics as a dictionary of key-value pairs. """ + # todo: implement concurrency for proc in psutil.process_iter(["pid", "name"]): - if proc.info["name"].lower() == process_name.lower(): - process = psutil.Process(proc.info["pid"]) - yield get_performance(process_name, process) - - -def get_performance(process_name: str, process: psutil.Process) -> Dict[str, int]: + if proc.name().lower() == process_name.lower(): + process = psutil.Process(proc.pid) + process._name = process_name + try: + perf_report = get_performance(process) + LOGGER.info({f"{process_name} [{process.pid}]": perf_report}) + perf_report["pname"] = process_name + perf_report["zombie"] = False + yield perf_report + except psutil.ZombieProcess as warn: + LOGGER.warning(warn) + yield {"zombie": True, "process_name": process_name} + + +def get_performance(process: psutil.Process) -> Dict[str, int | float]: """Checks performance by monitoring CPU utilization, number of threads and open files. Args: @@ -36,8 +46,9 @@ def get_performance(process_name: str, process: psutil.Process) -> Dict[str, int cpu = process.cpu_percent(interval=0.5) threads = process.num_threads() open_files = len(process.open_files()) - info_dict = {"cpu": cpu, "threads": threads, "open_files": open_files} - LOGGER.info({f"{process_name} [{process.pid}]": info_dict}) - info_dict["pid"] = process.pid.real - info_dict["pname"] = process_name - return info_dict + return { + "cpu": cpu, + "threads": threads, + "open_files": open_files, + "pid": process.pid.real, + } diff --git a/pyninja/routers.py b/pyninja/routers.py index dfb20e7..1709e17 100644 --- a/pyninja/routers.py +++ b/pyninja/routers.py @@ -1,21 +1,24 @@ import logging -import secrets import subprocess -from datetime import datetime from http import HTTPStatus from typing import List, Optional from fastapi import Depends, Header, Request from fastapi.responses import RedirectResponse from fastapi.routing import APIRoute +from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer -from pyninja import auth, database, exceptions, models, process, rate_limit, service +from pyninja import auth, exceptions, models, process, rate_limit, service, squire LOGGER = logging.getLogger("uvicorn.error") +security = HTTPBearer() async def run_command( - request: Request, payload: models.Payload, token: Optional[str] = Header(None) + request: Request, + payload: models.Payload, + apikey: HTTPAuthorizationCredentials = Depends(security), + token: Optional[str] = Header(None), ): """**API function to run a command on host machine.** @@ -28,70 +31,23 @@ async def run_command( APIResponse: Raises the HTTPStatus object with a status code and detail as response. """ - # todo: remove dependency authentication and convert it to condition match - # add failed auth to handle_auth_errors - # placeholder list, to avoid a DB search for every request - if request.client.host in models.session.forbid: - # Get timestamp until which the host has to be forbidden - if ( - timestamp := database.get_record(request.client.host) - ) and timestamp > auth.EPOCH(): - LOGGER.warning( - "%s is forbidden until %s due to repeated login failures", - request.client.host, - datetime.fromtimestamp(timestamp).strftime("%c"), - ) - raise exceptions.APIResponse( - status_code=HTTPStatus.FORBIDDEN.value, - detail=f"{request.client.host!r} is not allowed", - ) - if not all((models.env.remote_execution, models.env.api_secret)): - raise exceptions.APIResponse( - status_code=HTTPStatus.NOT_IMPLEMENTED.real, - detail="Remote execution has been disabled on the server.", - ) - if token and secrets.compare_digest(token, models.env.api_secret): - LOGGER.info( - "Command request '%s' received from client-host: %s, host-header: %s, x-fwd-host: %s", - payload.command, - request.client.host, - request.headers.get("host"), - request.headers.get("x-forwarded-host"), - ) - if user_agent := request.headers.get("user-agent"): - LOGGER.info("User agent: %s", user_agent) - else: - await auth.handle_auth_error(request) - raise exceptions.APIResponse( - status_code=HTTPStatus.UNAUTHORIZED.real, - detail=HTTPStatus.UNAUTHORIZED.phrase, - ) - process_cmd = subprocess.Popen( - payload.command, - shell=True, - universal_newlines=True, - text=True, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - ) - output = {"stdout": [], "stderr": []} + await auth.level_2(request, apikey, token) + LOGGER.info("Requested command: '%s'", payload.command) try: - stdout, stderr = process_cmd.communicate(timeout=payload.timeout) + response = squire.process_command(payload.command, payload.timeout) except subprocess.TimeoutExpired as warn: LOGGER.warning(warn) raise exceptions.APIResponse( status_code=HTTPStatus.REQUEST_TIMEOUT, detail=warn.__str__() ) - for line in stdout.splitlines(): - LOGGER.info(line.strip()) - output["stdout"].append(line.strip()) - for line in stderr.splitlines(): - LOGGER.error(line.strip()) - output["stderr"].append(line.strip()) - raise exceptions.APIResponse(status_code=HTTPStatus.OK.real, detail=output) + raise exceptions.APIResponse(status_code=HTTPStatus.OK.real, detail=response) -async def process_status(process_name: str): +async def process_status( + request: Request, + process_name: str, + apikey: HTTPAuthorizationCredentials = Depends(security), +): """**API function to monitor a process.** **Args:** @@ -103,6 +59,7 @@ async def process_status(process_name: str): APIResponse: Raises the HTTPStatus object with a status code and detail as response. """ + await auth.level_1(request, apikey) if response := list(process.get_process_status(process_name)): raise exceptions.APIResponse(status_code=HTTPStatus.OK.real, detail=response) LOGGER.error("%s: 404 - No such process", process_name) @@ -111,7 +68,11 @@ async def process_status(process_name: str): ) -async def service_status(service_name: str): +async def service_status( + request: Request, + service_name: str, + apikey: HTTPAuthorizationCredentials = Depends(security), +): """**API function to monitor a service.** **Args:** @@ -123,6 +84,7 @@ async def service_status(service_name: str): APIResponse: Raises the HTTPStatus object with a status code and detail as response. """ + await auth.level_1(request, apikey) response = service.get_service_status(service_name) LOGGER.info( "%s: %d - %s", @@ -153,12 +115,10 @@ def get_all_routes() -> List[APIRoute]: Returns the routes as a list of APIRoute objects. """ APIRoute(path="/", endpoint=docs, methods=["GET"], include_in_schema=False) - dependencies = [Depends(auth.authenticator)] - for each_rate_limit in models.env.rate_limit: - LOGGER.info("Adding rate limit: %s", each_rate_limit) - dependencies.append( - Depends(dependency=rate_limit.RateLimiter(each_rate_limit).init) - ) + dependencies = [ + Depends(dependency=rate_limit.RateLimiter(each_rate_limit).init) + for each_rate_limit in models.env.rate_limit + ] routes = [ APIRoute( path="/service-status", diff --git a/pyninja/squire.py b/pyninja/squire.py index 2ef9734..efc274d 100644 --- a/pyninja/squire.py +++ b/pyninja/squire.py @@ -1,11 +1,49 @@ import json +import logging import os import pathlib +import subprocess +from typing import Dict, List import yaml +from pydantic import PositiveFloat, PositiveInt from pyninja.models import EnvConfig +LOGGER = logging.getLogger("uvicorn.error") + + +def process_command( + command: str, timeout: PositiveInt | PositiveFloat +) -> Dict[str, List[str]]: + """Process the requested command. + + Args: + command: Command as string. + timeout: Timeout for the command. + + Returns: + Dict[str, List[str]]: + Returns the result with stdout and stderr as key-value pairs. + """ + process_cmd = subprocess.Popen( + command, + shell=True, + universal_newlines=True, + text=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + result = {"stdout": [], "stderr": []} + stdout, stderr = process_cmd.communicate(timeout=timeout) + for line in stdout.splitlines(): + LOGGER.info(line.strip()) + result["stdout"].append(line.strip()) + for line in stderr.splitlines(): + LOGGER.error(line.strip()) + result["stderr"].append(line.strip()) + return result + def env_loader(filename: str | os.PathLike) -> EnvConfig: """Loads environment variables based on filetypes.