Skip to content

Commit ab1c04a

Browse files
committed
Merges API and Manager to IntelMQ project.
1 parent d14611c commit ab1c04a

File tree

161 files changed

+102311
-2
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

161 files changed

+102311
-2
lines changed

.github/workflows/unittests.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ jobs:
6262
run: bash .github/workflows/scripts/setup-full.sh
6363

6464
- name: Install test dependencies
65-
run: pip install pytest-cov Cerberus requests_mock coverage
65+
run: pip install pytest-cov Cerberus requests_mock coverage httpx pycodestyle
6666

6767
- name: Install dependencies
6868
if: ${{ matrix.type == 'basic' }}

intelmq/__main__.py

+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import argparse
2+
import getpass
3+
import sys
4+
5+
import uvicorn
6+
7+
from intelmq.api.config import Config
8+
from intelmq.api.session import SessionStore
9+
from intelmq.lib import utils
10+
11+
12+
def server_start(host: str = None, port: int = None, debug: bool = False, *args, **kwargs):
13+
server_settings = utils.get_server_settings()
14+
host = host if host is not None else server_settings.get("host", "0.0.0.0")
15+
port = int(port) if port is not None else int(server_settings.get("port", 8080))
16+
17+
return uvicorn.run(
18+
"intelmq.server:app",
19+
host=host,
20+
reload=debug,
21+
port=port,
22+
workers=1,
23+
)
24+
25+
26+
def server_adduser(username: str, password: str = None, *args, **kwargs):
27+
api_config: Config = Config()
28+
29+
if api_config.session_store is None:
30+
print("Could not add user- no session store configured in configuration!", file=sys.stderr)
31+
exit(1)
32+
33+
session_store = SessionStore(str(api_config.session_store), api_config.session_duration)
34+
password = getpass.getpass() if password is None else password
35+
session_store.add_user(username, password)
36+
print(f"Added user {username} to intelmq session file.")
37+
38+
39+
def main():
40+
parser = argparse.ArgumentParser(prog="intelmq", usage="intelmq [OPTIONS] COMMAND")
41+
parser.set_defaults(func=(lambda *_, **__: parser.print_help())) # wrapper to accept args and kwargs
42+
parser._optionals.title = "Options"
43+
parser.add_argument("--version", action="store_true", help="print version and exit", default=None)
44+
commands = parser.add_subparsers(metavar="", title="Commands")
45+
46+
# intelmq server
47+
srv_parser = commands.add_parser("server", help="server subcommands", usage="intelmq server [COMMAND]")
48+
srv_parser.set_defaults(func=(lambda *_, **__: srv_parser.print_help())) # wrapper to accept args and kwargs
49+
srv_parser._optionals.title = "Options"
50+
srv_subcommands = srv_parser.add_subparsers(metavar="", title="Commands")
51+
52+
# intelmq server start
53+
srv_start = srv_subcommands.add_parser("start", help="start the server", usage="intelmq server start [OPTIONS]")
54+
srv_start.set_defaults(func=server_start)
55+
srv_start._optionals.title = "Options"
56+
srv_start.add_argument("--debug", action="store_true", dest="debug", default=None)
57+
srv_start.add_argument("--host", type=str, dest="host")
58+
srv_start.add_argument("--port", type=int, dest="port")
59+
60+
# intelmq server adduser
61+
srv_adduser = srv_subcommands.add_parser("adduser", help="adds new user", usage="intelmq server adduser [OPTIONS]")
62+
srv_adduser.set_defaults(func=server_adduser)
63+
srv_adduser._optionals.title = "Options"
64+
srv_adduser.add_argument('--username', required=True, help='The username of the account.', type=str)
65+
srv_adduser.add_argument('--password', required=False, help='The password of the account.', type=str)
66+
67+
args = parser.parse_args()
68+
return args.func(**vars(args))
69+
70+
71+
if __name__ == "__main__":
72+
main()

intelmq/api/__init__.py

Whitespace-only changes.

intelmq/api/config.py

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
"""Configuration for IntelMQ Manager
2+
3+
SPDX-FileCopyrightText: 2020 Intevation GmbH <https://intevation.de>
4+
SPDX-License-Identifier: AGPL-3.0-or-later
5+
6+
Funding: of initial version by SUNET
7+
Author(s):
8+
* Bernhard Herzog <bernhard.herzog@intevation.de>
9+
"""
10+
11+
from typing import List, Optional
12+
from pathlib import Path
13+
from intelmq.lib import utils
14+
15+
16+
class Config:
17+
18+
"""Configuration settings for IntelMQ Manager"""
19+
20+
intelmq_ctl_cmd: List[str] = ["sudo", "-u", "intelmq", "/usr/local/bin/intelmqctl"]
21+
22+
allowed_path: Path = Path("/opt/intelmq/var/lib/bots/")
23+
24+
session_store: Optional[Path] = None
25+
26+
session_duration: int = 24 * 3600
27+
28+
allow_origins: List[str] = ['*']
29+
30+
enable_webgui: bool = True
31+
32+
host: str = "0.0.0.0"
33+
34+
port: int = 8080
35+
36+
def __init__(self):
37+
server_settings = utils.get_server_settings()
38+
39+
if "intelmq_ctl_cmd" in server_settings:
40+
self.intelmq_ctl_cmd = server_settings["intelmq_ctl_cmd"]
41+
42+
if "allowed_path" in server_settings:
43+
self.allowed_path = Path(server_settings["allowed_path"])
44+
45+
if "session_store" in server_settings:
46+
self.session_store = Path(server_settings["session_store"])
47+
48+
if "session_duration" in server_settings:
49+
self.session_duration = int(server_settings["session_duration"])
50+
51+
if "allow_origins" in server_settings:
52+
self.allow_origins = server_settings['allow_origins']
53+
54+
if "enable_webgui" in server_settings:
55+
self.enable_webgui = server_settings["enable_webgui"]
56+
57+
if "host" in server_settings:
58+
self.host = server_settings["host"]
59+
60+
if "port" in server_settings:
61+
self.host = server_settings["port"]

intelmq/api/dependencies.py

+62
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"""Dependencies of the API endpoints, in the FastAPI style
2+
3+
SPDX-FileCopyrightText: 2022 CERT.at GmbH <https://cert.at>
4+
SPDX-License-Identifier: AGPL-3.0-or-later
5+
"""
6+
7+
import typing
8+
from typing import Generic, Optional, TypeVar
9+
10+
from fastapi import Depends, Header, HTTPException, Response, status
11+
12+
import intelmq.api.config
13+
import intelmq.api.session as session
14+
15+
T = TypeVar("T")
16+
17+
18+
class OneTimeDependency(Generic[T]):
19+
"""Allows one-time explicit initialization of the dependency,
20+
and then returning it on every usage.
21+
22+
It emulates the previous behavior that used global variables"""
23+
24+
def __init__(self) -> None:
25+
self._value: Optional[T] = None
26+
27+
def initialize(self, value: T) -> None:
28+
self._value = value
29+
30+
def __call__(self) -> Optional[T]:
31+
return self._value
32+
33+
34+
api_config = OneTimeDependency[intelmq.api.config.Config]()
35+
session_store = OneTimeDependency[session.SessionStore]()
36+
37+
38+
def cached_response(max_age: int):
39+
"""Adds the cache headers to the response"""
40+
def _cached_response(response: Response):
41+
response.headers["cache-control"] = f"max-age={max_age}"
42+
return _cached_response
43+
44+
45+
def token_authorization(authorization: typing.Union[str, None] = Header(default=None),
46+
session: session.SessionStore = Depends(session_store)):
47+
if session is not None:
48+
if not authorization or not session.verify_token(authorization):
49+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail={
50+
"Authentication Required":
51+
"Please provide valid Token verification credentials"
52+
})
53+
54+
55+
def startup(config: intelmq.api.config.Config):
56+
"""A starting point to one-time initialization of necessary dependencies. This needs to
57+
be called by the application on the startup."""
58+
api_config.initialize(config)
59+
session_file = config.session_store
60+
if session_file is not None:
61+
session_store.initialize(session.SessionStore(str(session_file),
62+
config.session_duration))

intelmq/api/exceptions.py

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
"""Exception handlers for API
2+
3+
SPDX-FileCopyrightText: 2022 CERT.at GmbH <https://cert.at>
4+
SPDX-License-Identifier: AGPL-3.0-or-later
5+
"""
6+
7+
from fastapi import FastAPI, Request, status
8+
from fastapi.responses import JSONResponse
9+
from starlette.exceptions import HTTPException as StarletteHTTPException
10+
11+
import intelmq.api.runctl as runctl
12+
13+
14+
def ctl_error_handler(request: Request, exc: runctl.IntelMQCtlError):
15+
return JSONResponse(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, content=exc.error_dict)
16+
17+
18+
def handle_generic_error(request: Request, exc: StarletteHTTPException):
19+
return JSONResponse(status_code=exc.status_code, content={"error": exc.detail})
20+
21+
22+
def register(app: FastAPI):
23+
"""A hook to register handlers in the app. Need to be called before startup"""
24+
app.add_exception_handler(runctl.IntelMQCtlError, ctl_error_handler)
25+
app.add_exception_handler(StarletteHTTPException, handle_generic_error)

intelmq/api/files.py

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
"""Direct access to IntelMQ files and directories
2+
3+
SPDX-FileCopyrightText: 2020 Intevation GmbH <https://intevation.de>
4+
SPDX-License-Identifier: AGPL-3.0-or-later
5+
6+
Funding: of initial version by SUNET
7+
Author(s):
8+
* Bernhard Herzog <bernhard.herzog@intevation.de>
9+
10+
This module implements the part of the IntelMQ-Manager backend that
11+
allows direct read and write access to some of the files used by
12+
IntelMQ.
13+
"""
14+
15+
from pathlib import PurePath, Path
16+
from typing import Optional, Tuple, Union, Dict, Any, Iterable, BinaryIO
17+
18+
from intelmq.api.config import Config
19+
20+
21+
def path_starts_with(path: PurePath, prefix: PurePath) -> bool:
22+
"""Return whether the path starts with prefix.
23+
24+
Both arguments must be absolute paths. If not, this function raises
25+
a ValueError.
26+
27+
This function compares the path components, so it's not a simple
28+
string prefix test.
29+
"""
30+
if not path.is_absolute():
31+
raise ValueError("{!r} is not absolute".format(path))
32+
if not prefix.is_absolute():
33+
raise ValueError("{!r} is not absolute".format(prefix))
34+
return path.parts[:len(prefix.parts)] == prefix.parts
35+
36+
37+
class FileAccess:
38+
39+
def __init__(self, config: Config):
40+
self.allowed_path = config.allowed_path
41+
42+
def file_name_allowed(self, filename: str) -> Optional[Tuple[bool, Path]]:
43+
"""Determine wether the API should allow access to a file."""
44+
resolved = Path(filename).resolve()
45+
if not path_starts_with(resolved, self.allowed_path):
46+
return None
47+
48+
return (False, resolved)
49+
50+
def load_file_or_directory(self, unvalidated_filename: str, fetch: bool) \
51+
-> Union[Tuple[str, Union[BinaryIO, Dict[str, Any]]], None]:
52+
allowed = self.file_name_allowed(unvalidated_filename)
53+
if allowed is None:
54+
return None
55+
56+
content_type = "application/json"
57+
predefined, normalized = allowed
58+
59+
if predefined or fetch:
60+
if fetch:
61+
content_type = "text/html"
62+
return (content_type, open(normalized, "rb"))
63+
64+
result = {"files": {}} # type: Dict[str, Any]
65+
if normalized.is_dir():
66+
result["directory"] = str(normalized)
67+
files = normalized.iterdir() # type: Iterable[Path]
68+
else:
69+
files = [normalized]
70+
71+
for path in files:
72+
stat = path.stat()
73+
if stat.st_size < 2000:
74+
# FIXME: don't hardwire this size
75+
obj = {"contents": path.read_text()} # type: Dict[str, Any]
76+
else:
77+
obj = {"size": stat.st_size, "path": str(path.resolve())}
78+
result["files"][path.name] = obj
79+
return (content_type, result)

intelmq/api/models.py

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
"""Models used in API
2+
3+
SPDX-FileCopyrightText: 2023 CERT.at GmbH <https://cert.at/>
4+
SPDX-License-Identifier: AGPL-3.0-or-later
5+
"""
6+
7+
from pydantic import BaseModel
8+
9+
10+
class TokenResponse(BaseModel):
11+
login_token: str
12+
username: str

0 commit comments

Comments
 (0)