-
Notifications
You must be signed in to change notification settings - Fork 10
POC fastapi #167
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
POC fastapi #167
Changes from 18 commits
eedb8d7
761d571
9969303
86993e6
7f5f250
5dc058c
e699992
f1ff0ff
8d3b804
f796692
678f131
8ea042c
41109ec
0213abf
19f0745
70ce073
5fab494
a778ff3
3318f6a
44fb3c3
352b9f4
c9ed739
614c111
11f6095
11cf68c
a38426b
83fee89
d14a3e1
b95a520
0037313
5269124
8c3b196
0c305f0
51c48f4
9cd2297
bf4b0b1
4cd8529
82ac2f8
430205f
75a7d65
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,17 @@ | ||
| from passlib.context import CryptContext | ||
|
|
||
| pwd_context = CryptContext(schemes=['bcrypt'], deprecated='auto') | ||
|
|
||
|
|
||
| users = { | ||
| 'user': 'user', | ||
| 'admin': 'admin', | ||
| } | ||
|
|
||
| env_content = '' | ||
| print('\nCopy the following lines into your .env file:\n') | ||
| for username, plain_password in users.items(): | ||
| hashed_password = pwd_context.hash(plain_password) | ||
| env_line = f"USER_{username.upper()}_HASH=\"{hashed_password}\"" | ||
| print(env_line) | ||
| env_content += env_line + '\n' | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,77 @@ | ||
| pip install fastapi uvicorn httpx PyYAML pydantic | ||
| bcrypt==3.2.2 !! otherwise passlib complains | ||
|
|
||
|
|
||
| uvicorn remote_rest.main:app --host 0.0.0.0 --port 8000 --reload | ||
|
|
||
| [] TODO sort out logs for different instances | ||
|
|
||
| [] TODO return logs to caller | ||
|
|
||
| [] TODO limit max. concurent instance number | ||
|
|
||
| [] TODO concurrency for several instances? | ||
|
|
||
| [] TODO add authentication | ||
|
|
||
| [] TODO queue requests for instances | ||
|
|
||
| [] TODO dynamic configuration of attackmate config | ||
|
|
||
| [] TODO make logging (debug, json etc) configurable at runtime (endpoint or user query paramaters?) | ||
|
|
||
| [] TODO ALLOWED_PLAYBOOK_DIR -> define in and load from configs | ||
|
|
||
| [] TODO add swagger examples | ||
|
|
||
| [] TODO generate/check OpenAPI schema | ||
|
|
||
| [x] TODO seperate router modules? | ||
|
|
||
|
|
||
|
|
||
|
|
||
|
|
||
| # Execute a playbook by sending its YAML content (uses a temporary instance) | ||
| python -m remote_rest.client playbook-yaml examples/playbook.yml | ||
|
|
||
| # Request the server execute a playbook from its allowed directory | ||
| Ensure 'playbook.yml' exists in server's ALLOWED_PLAYBOOK_DIR | ||
|
|
||
| python -m remote_rest.client playbook-file safe_playbook.yml | ||
|
|
||
|
|
||
| # Single Command Execution (on a persistent Instance) | ||
|
|
||
| ## Shell Command | ||
| ```bash | ||
| python -m remote_rest.client command shell 'echo "Hello"' | ||
| ``` | ||
|
|
||
| ### Run a command in the background or with metadata | ||
| ```bash | ||
| python -m remote_rest.client command shell 'echo hello' --metadata tactic=recon --metadata technique=TXXX | ||
| ``` | ||
|
|
||
| ## Run a Sleep Command (Background): | ||
| ```bash | ||
| python -m remote_rest.client command sleep --seconds 8 --background | ||
| ``` | ||
| # (Client returns immediately, server sleeps) | ||
|
|
||
|
|
||
| # Certificate generation | ||
| preliminary, automate later? | ||
| with open ssl | ||
| ```bash | ||
| openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -nodes | ||
| ``` | ||
|
|
||
| Common Name: localhost (or ip adress the server will be) | ||
|
|
||
|
|
||
| running client: | ||
|
|
||
| ```bash | ||
| python -m client --cacert <path_to_cert> login user user | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,103 @@ | ||
| import logging | ||
| import os | ||
| import secrets | ||
| from datetime import datetime, timedelta, timezone | ||
| from typing import Any, Dict, Optional | ||
|
|
||
| from dotenv import load_dotenv | ||
| from fastapi import Depends, HTTPException, status | ||
| from fastapi.security import APIKeyHeader | ||
| from passlib.context import CryptContext | ||
|
|
||
| load_dotenv() | ||
|
|
||
|
|
||
| TOKEN_EXPIRE_MINUTES = int(os.getenv('TOKEN_EXPIRE_MINUTES', 30)) | ||
| API_KEY_HEADER_NAME = 'X-Auth-Token' | ||
| api_key_header_scheme = APIKeyHeader(name=API_KEY_HEADER_NAME, auto_error=True) | ||
| pwd_context = CryptContext(schemes=['bcrypt'], deprecated='auto') | ||
|
|
||
| # In-Memory token Store | ||
| # token looks like this {"username": str, "expires": datetime} | ||
| # state is lost on server restart. | ||
| # Not inherently thread-safe for multi-worker setups without locks ? | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe we will switch at some point to jwt. I guess that would be thread-safe, since we don't have any state anymore |
||
| ACTIVE_TOKENS: Dict[str, Dict[str, Any]] = {} | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| def verify_password(plain_password: str, hashed_password: str) -> bool: | ||
| return pwd_context.verify(plain_password, hashed_password) | ||
|
|
||
|
|
||
| def get_user_hash(username: str) -> Optional[str]: | ||
| """Fetches the hashed password from environment variables.""" | ||
| env_var_name = f"USER_{username.upper()}_HASH" | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. looks like something for pydantic-settings |
||
| return os.getenv(env_var_name) | ||
|
|
||
|
|
||
| def create_access_token(username: str) -> str: | ||
| """Creates a new token, stores it, and returns the token string.""" | ||
| token = secrets.token_urlsafe(32) | ||
| expires = datetime.now(timezone.utc) + timedelta(minutes=TOKEN_EXPIRE_MINUTES) | ||
| # TODO locking needed for multi-threaded access, smth like with token_lock ? | ||
| ACTIVE_TOKENS[token] = {'username': username, 'expires': expires} | ||
| logger.info(f"Created new token for user '{username}' expiring at {expires}") | ||
| return token | ||
|
|
||
|
|
||
| def renew_token_expiry(token: str) -> bool: | ||
| """Updates the expiry time for an existing token. Returns True if successful.""" | ||
| token_data = ACTIVE_TOKENS.get(token) | ||
| if token_data: | ||
| token_data['expires'] = datetime.now(timezone.utc) + timedelta(minutes=TOKEN_EXPIRE_MINUTES) | ||
| logger.debug(f"Renewed token expiry for user '{token_data['username']}'") | ||
| return True | ||
| return False | ||
|
|
||
|
|
||
| def cleanup_expired_tokens(): | ||
| """Removes expired tokens from the store""" | ||
| now = datetime.now(timezone.utc) | ||
| expired_tokens = [token for token, data in ACTIVE_TOKENS.items() if data['expires'] < now] | ||
| for token in expired_tokens: | ||
| username = ACTIVE_TOKENS.get(token, {}).get('username', 'unknown') | ||
| del ACTIVE_TOKENS[token] | ||
| logger.info(f"Removed expired token for user '{username}'.") | ||
|
|
||
|
|
||
| # Authentication Dependency -> this gets passed to the routes | ||
| async def get_current_user(token: str = Depends(api_key_header_scheme)) -> str: | ||
| """ | ||
| validate token and return the username | ||
| renews the token's expiration on successful validation | ||
| cleanup of expired tokens. | ||
| """ | ||
|
|
||
| cleanup_expired_tokens() | ||
|
|
||
| credentials_exception = HTTPException( | ||
| status_code=status.HTTP_401_UNAUTHORIZED, | ||
| detail='Invalid authentication credentials', | ||
| headers={'WWW-Authenticate': 'Bearer'}, | ||
| ) | ||
|
|
||
| token_data = ACTIVE_TOKENS.get(token) | ||
| if not token_data: | ||
| logger.warning(f"Token not found: {token[:5]}...") | ||
| raise credentials_exception | ||
|
|
||
| username: str = token_data['username'] | ||
| expires: datetime = token_data['expires'] | ||
|
|
||
| if expires < datetime.now(timezone.utc): | ||
| logger.warning(f"Token expired for user '{username}'") | ||
| # Remove the expired token | ||
| if token in ACTIVE_TOKENS: | ||
| del ACTIVE_TOKENS[token] | ||
| raise credentials_exception | ||
|
|
||
| renew_token_expiry(token) | ||
|
|
||
| logger.debug(f"Token validated successfully for user: {username}") | ||
| return username | ||
Uh oh!
There was an error while loading. Please reload this page.