Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
eedb8d7
poc fastapi
thorinaboenke May 2, 2025
761d571
create routers, create global state with DI
thorinaboenke May 2, 2025
9969303
isort
thorinaboenke May 2, 2025
86993e6
add readme
thorinaboenke May 2, 2025
7f5f250
add fastapi to dependencies
thorinaboenke May 5, 2025
5dc058c
use only one persistent instance
thorinaboenke May 5, 2025
e699992
give transient instances a uuid and return in response
thorinaboenke May 5, 2025
f1ff0ff
add examples to readme
thorinaboenke May 5, 2025
8d3b804
add instance logging with debug parameter
thorinaboenke May 5, 2025
f796692
return logfile content on playbook execution
thorinaboenke May 5, 2025
678f131
create hash utility
thorinaboenke May 6, 2025
8ea042c
new response schemas
thorinaboenke May 6, 2025
41109ec
authentication with Token
thorinaboenke May 6, 2025
0213abf
Merge branch 'development' into poc_fastapi
thorinaboenke May 6, 2025
19f0745
fastapi dependency!
thorinaboenke May 6, 2025
70ce073
Merge branch 'poc_fastapi' of github.com:thorinaboenke/attackmate int…
thorinaboenke May 6, 2025
5fab494
self signed ssl keys
thorinaboenke May 7, 2025
a778ff3
generate hash utility
thorinaboenke May 7, 2025
3318f6a
initial remote command and executor
thorinaboenke May 7, 2025
44fb3c3
add loop command and type alias to remote command schema
thorinaboenke May 13, 2025
352b9f4
structure for remote command and executor
thorinaboenke May 13, 2025
c9ed739
initial remote client class
thorinaboenke May 13, 2025
614c111
use argon2
thorinaboenke May 19, 2025
11f6095
handle json logging of remote_command
thorinaboenke May 19, 2025
11cf68c
fix types
thorinaboenke May 22, 2025
a38426b
Merge remote-tracking branch 'origin/development' into poc_fastapi
thorinaboenke Jul 15, 2025
83fee89
add json to remote logging
thorinaboenke Jul 15, 2025
d14a3e1
add json to remote logging
thorinaboenke Jul 15, 2025
b95a520
avoid duplicating stream handler
thorinaboenke Jul 15, 2025
0037313
move api logging setup out of mein
thorinaboenke Jul 17, 2025
5269124
add dependencies
thorinaboenke Jul 17, 2025
8c3b196
single endpoint for commands
thorinaboenke Jul 17, 2025
0c305f0
improve logging setup
thorinaboenke Jul 19, 2025
51c48f4
use global variable for log file name
thorinaboenke Jul 21, 2025
9cd2297
remove single command endpoints
thorinaboenke Jul 21, 2025
bf4b0b1
refactor command schema imports
thorinaboenke Jul 21, 2025
4cd8529
refactor remote executor
thorinaboenke Jul 23, 2025
82ac2f8
variable naming
thorinaboenke Aug 1, 2025
430205f
remove comment
thorinaboenke Aug 1, 2025
75a7d65
Use local inline HTML for the browser-tests
annaerdi Aug 14, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions create_hashes.py
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')
Comment thread
thorinaboenke marked this conversation as resolved.
Outdated


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'
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ dependencies = [
"httpx[http2]",
"vncdotool",
"pytest-mock",
"fastapi",
"playwright"
]
dynamic = ["version"]
Expand Down
77 changes: 77 additions & 0 deletions remote_rest/README.md
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
```
Empty file added remote_rest/__init__.py
Empty file.
103 changes: 103 additions & 0 deletions remote_rest/auth_utils.py
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 ?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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
Loading