Skip to content

Commit 6bf7c94

Browse files
committed
4.4.2 Release
1 parent b436139 commit 6bf7c94

File tree

196 files changed

+9006
-2232
lines changed

Some content is hidden

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

196 files changed

+9006
-2232
lines changed

conversion/database_migration/bulk_alert_data.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,16 +51,18 @@ def transform_alert(alert=None, schema_id_map=None):
5151
if k =='columns' or k =='search' or k=='_raw':
5252
# We don't care about these columns because they should not show up in an alertgroup table
5353
continue
54-
else:
54+
else:
5555
# Get the schem key id from the map we created beforehand
5656
schema_id = schema_id_map.get(f"{k}-{alertgroup_id}")
5757
if schema_id is None:
5858
continue
5959
else:
6060
data_value = alert['data'].get(k)
6161
data_value_flaired = alert['data_with_flair'].get(k)
62-
data_value = json.dumps(data_value)
63-
data_value_flaired = json.dumps(data_value_flaired)
62+
if not isinstance(data_value, str):
63+
data_value = json.dumps(data_value)
64+
if not isinstance(data_value_flaired, str):
65+
data_value_flaired = json.dumps(data_value_flaired)
6466
alert_data = [data_value, data_value_flaired, schema_id , alert['id']]
6567
alert_datas.append(alert_data)
6668
return alert_datas

requirements.txt

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,25 @@
1-
fastapi==0.112.1
1+
fastapi[standard]==0.115.12
22
SQLAlchemy==2.0.32
33
pydantic_core==2.23.4
44
pydantic_settings==2.4.0
55
pydantic==2.9.2
6-
mysql-connector-python==9.1.0
6+
mysqlclient==2.2.7
77
meilisearch==0.31.4
88
msal==1.30.0
99
python-dateutil==2.9.0.post0
1010
nh3==0.2.18
11-
python-jose==3.3.0
11+
python-jose==3.4.0
1212
loguru==0.7.2
1313
email_validator==2.0.0
14-
Jinja2==3.1.4
14+
Jinja2==3.1.5
1515
ldap3==2.9.1
1616
passlib==1.7.4
17-
streaming-form-data==1.16.0
17+
streaming-form-data==1.19.0
1818
boto3==1.35.0
1919
tabulate==0.9.0
2020
markdownify==0.13.1
2121
xhtml2pdf==0.2.16
2222
pdf2docx==0.5.8
23-
sse-starlette==2.1.3
24-
python-multipart==0.0.9
23+
sse-starlette==2.2.1
24+
python-multipart==0.0.19
25+
pyzipper==0.3.6

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ def get_requirements():
99

1010
setup(
1111
name="SCOT",
12-
version="4.2.0",
12+
version="4.4.0",
1313
description="Sandia Cyber Omni Tracker Server",
1414
packages=find_packages("src"),
1515
package_dir={"": "src"},

src/app/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "4.0.0"
1+
__version__ = "4.4.0"

src/app/api/__init__.py

Lines changed: 86 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
1+
from logging.config import dictConfig
12
from fastapi import FastAPI, Request
23
from fastapi.middleware.cors import CORSMiddleware
4+
from fastapi.middleware.gzip import GZipMiddleware
35
from fastapi.openapi.utils import get_openapi
6+
from fastapi.openapi.docs import (
7+
get_redoc_html,
8+
get_swagger_ui_html
9+
)
10+
from fastapi.staticfiles import StaticFiles
411
from starlette.middleware.base import BaseHTTPMiddleware
12+
from starlette.responses import PlainTextResponse
13+
from sqlalchemy.exc import TimeoutError, DatabaseError
514

615
from app.api.endpoints import router
716
from app.core.config import settings
@@ -12,19 +21,27 @@ async def db_middleware(request: Request, call_next) -> None:
1221
"""
1322
Middleware that wraps each API call in a transaction
1423
"""
15-
with SessionLocal() as db_session:
16-
request.state.db = db_session
24+
ROUTE_PREFIX_WHITELIST = ("/api/v1/firehose")
25+
if request.url.path.startswith(ROUTE_PREFIX_WHITELIST):
26+
return await call_next(request)
27+
else:
28+
try:
29+
with SessionLocal() as db_session:
30+
request.state.db = db_session
31+
response = await call_next(request)
32+
if response.status_code >= 500:
33+
db_session.rollback()
34+
else:
1735

18-
response = await call_next(request)
19-
if response.status_code >= 500:
20-
db_session.rollback()
21-
else:
36+
db_session.commit()
2237

23-
db_session.commit()
24-
25-
if hasattr(request.state, "audit_logger") and request.state.audit_logger:
26-
request.state.audit_logger.save_audits(db_session)
27-
db_session.commit()
38+
if hasattr(request.state, "audit_logger") and request.state.audit_logger:
39+
request.state.audit_logger.save_audits(db_session)
40+
db_session.commit()
41+
except TimeoutError:
42+
return TextResponse(503, "Timeout when connecting to database")
43+
except DatabaseError:
44+
return TextResponse(500, "Error when connecting to database")
2845

2946
return response
3047

@@ -33,18 +50,42 @@ def create_app() -> FastAPI:
3350
"""
3451
:return:
3552
"""
53+
# Set up logging format
54+
# dictConfig({
55+
# "version": 1,
56+
# "disable_existing_loggers": False,
57+
# "formatters": {
58+
# "access": {
59+
# "()": "uvicorn.logging.AccessFormatter",
60+
# "fmt": '%(asctime)s - %(levelprefix)s %(client_addr)s - \"%(request_line)s\" %(status_code)s',
61+
# "use_colors": True
62+
# },
63+
# },
64+
# "handlers": {
65+
# "access": {
66+
# "formatter": "access",
67+
# "class": "logging.StreamHandler",
68+
# "stream": "ext://sys.stdout",
69+
# },
70+
# },
71+
# "loggers": {
72+
# "uvicorn.access": {
73+
# "handlers": ["access"],
74+
# "level": "INFO",
75+
# "propagate": False
76+
# },
77+
# },
78+
# })
79+
3680
app = FastAPI(
3781
debug=settings.DEBUG,
38-
title=settings.TITLE,
39-
description=settings.DESCRIPTION,
40-
docs_url=settings.DOCS_URL,
41-
openapi_url=settings.OPENAPI_URL,
42-
redoc_url=settings.REDOC_URL,
43-
swagger_ui_parameters={
44-
"filter": True
45-
}
82+
docs_url=None,
83+
redoc_url=None,
84+
openapi_url=settings.OPENAPI_URL
4685
)
4786

87+
app.mount('/api/static', StaticFiles(packages=[('app.api', 'static')]), name='static')
88+
4889
app.include_router(router.api_router, prefix=settings.API_V1_STR)
4990

5091
app.add_middleware(
@@ -56,13 +97,18 @@ def create_app() -> FastAPI:
5697
allow_headers=["*"],
5798
)
5899

100+
app.add_middleware(
101+
GZipMiddleware,
102+
minimum_size=500,
103+
compresslevel=5)
104+
59105
app.add_middleware(BaseHTTPMiddleware, dispatch=db_middleware)
60106

61107
if not app.openapi_schema:
62108
# will need to update all links for open source stuff
63109
openapi_schema = get_openapi(
64110
title="SCOT",
65-
version="4.2.0",
111+
version="4.4.0",
66112
summary="Sandia Cyber Omni Tracker Server",
67113
description="The Sandia Cyber Omni Tracker (SCOT) is a cyber security incident response management system and knowledge base. Designed by cyber security incident responders, SCOT provides a new approach to manage security alerts, analyze data for deeper patterns, coordinate team efforts, and capture team knowledge. SCOT integrates with existing security applications to provide a consistent, easy to use interface that enhances analyst effectiveness.",
68114
routes=app.routes,
@@ -82,4 +128,24 @@ def create_app() -> FastAPI:
82128
}
83129
app.openapi_schema = openapi_schema
84130

131+
@app.get(settings.DOCS_URL, include_in_schema=False)
132+
async def custom_swagger_ui_html():
133+
return get_swagger_ui_html(
134+
openapi_url=settings.OPENAPI_URL,
135+
title=settings.TITLE,
136+
swagger_js_url=settings.SWAGGER_JS_URL,
137+
swagger_css_url=settings.SWAGGER_CSS_URL,
138+
swagger_ui_parameters={
139+
"filter": True
140+
}
141+
)
142+
143+
@app.get(settings.REDOC_URL, include_in_schema=False)
144+
async def redoc_html():
145+
return get_redoc_html(
146+
openapi_url=settings.OPENAPI_URL,
147+
title=settings.TITLE,
148+
redoc_js_url=settings.REDOC_JS_URL,
149+
)
150+
85151
return app

src/app/api/deps.py

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import json
22
from datetime import datetime, timedelta
3+
from typing import Annotated
34

4-
from fastapi import Depends, HTTPException, Request
5+
from fastapi import Depends, HTTPException, Request, Query, Path
56
from fastapi.encoders import jsonable_encoder
67
from jose import jwt
78
from pydantic import ValidationError
@@ -77,6 +78,23 @@ def get_token_data(
7778
return token_data
7879

7980

81+
def get_token_data_no_apikey(
82+
token: str = Depends(reusable_oauth2),
83+
) -> schemas.TokenPayload:
84+
try:
85+
if token:
86+
payload = jwt.decode(
87+
token, settings.SECRET_KEY, algorithms=[security.ALGORITHM]
88+
)
89+
token_data = schemas.TokenPayload(**payload)
90+
else:
91+
token_data = None
92+
except (jwt.JWTError, ValidationError):
93+
# Token validation failed; continue as if there's no token
94+
token_data = None
95+
return token_data
96+
97+
8098
def get_current_user(
8199
request: Request,
82100
db: Session = Depends(get_db),
@@ -247,7 +265,8 @@ class PermissionCheck:
247265
type_allow_whitelist = [TargetTypeEnum.entity, TargetTypeEnum.tag,
248266
TargetTypeEnum.source, TargetTypeEnum.entity_class,
249267
TargetTypeEnum.pivot, TargetTypeEnum.entity_type,
250-
TargetTypeEnum.none, None]
268+
TargetTypeEnum.special_metric, TargetTypeEnum.none,
269+
None]
251270

252271
def __init__(self, target_type, permission, allow_admin=True):
253272
self.target_type = target_type
@@ -290,15 +309,26 @@ def check_permissions(self, db, roles, user, target_id=None):
290309
class PermissionCheckId(PermissionCheck):
291310
def __call__(
292311
self,
293-
id: int,
312+
id: Annotated[int, Path(...)],
294313
db: Session = Depends(get_db),
295314
user: models.User = Depends(get_current_active_user),
296315
roles: list[models.Role] = Depends(get_current_roles),
297316
):
298317
if not self.check_permissions(db, roles, user, id):
299-
raise HTTPException(
300-
status_code=403,
301-
detail="You do not have permission to access this resource, or "
302-
"it does not exist",
303-
)
318+
raise HTTPException(403, "You do not have permission to access this resource, or it does not exist")
319+
return True
320+
321+
322+
# Same as above, except for many objects
323+
class PermissionCheckIds(PermissionCheck):
324+
def __call__(
325+
self,
326+
ids: Annotated[list[int], Query(...)],
327+
db: Session = Depends(get_db),
328+
user: models.User = Depends(get_current_active_user),
329+
roles: list[models.Role] = Depends(get_current_roles),
330+
):
331+
for id in ids:
332+
if not self.check_permissions(db, roles, user, id):
333+
raise HTTPException(403, "You do not have permission to access this resource, or it does not exist")
304334
return True

src/app/api/endpoints/alert.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from typing import Any, Annotated
2-
from fastapi import APIRouter, Depends, HTTPException, Body, Path
2+
from fastapi import APIRouter, Depends, HTTPException, Body, Path, Query
33
from sqlalchemy.orm import Session
44
from pydantic import ValidationError
55

@@ -37,6 +37,27 @@
3737
description, examples = create_schema_details(schemas.AlertUpdate)
3838

3939

40+
@router.put(
41+
"/many",
42+
response_model=list[schemas.Alert],
43+
summary="Update many alerts",
44+
description=description
45+
)
46+
def update_alerts(
47+
*,
48+
db: Session = Depends(deps.get_db),
49+
audit_logger: deps.AuditLogger = Depends(deps.get_audit_logger),
50+
current_user: models.User = Depends(deps.get_current_active_user),
51+
current_roles: list[models.Role] = Depends(deps.get_current_roles),
52+
ids: Annotated[list[int], Query(...)],
53+
obj: Annotated[schemas.AlertUpdate, Body(..., openapi_examples=examples)],
54+
) -> Any:
55+
_objs = []
56+
for id in ids:
57+
_objs.append(update_alert(db=db, audit_logger=audit_logger, current_user=current_user, current_roles=current_roles, id=id, obj=obj))
58+
return _objs
59+
60+
4061
# Custom PUT so that you can modify alerts if you have access to the alertgroup
4162
@router.put(
4263
"/{id}",

src/app/api/endpoints/alertgroup.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,24 +19,26 @@
1919
generic_search,
2020
generic_export,
2121
generic_upvote_and_downvote,
22-
generic_user_links
22+
generic_user_links,
23+
generic_tag_untag
2324
)
2425

2526
router = APIRouter()
2627

2728
# Create get, post, put, and delete endpoints
28-
generic_export(router, crud.alert_group, TargetTypeEnum.alertgroup)
2929
generic_get(router, crud.alert_group, TargetTypeEnum.alertgroup, schemas.AlertGroupDetailed)
3030
generic_post(router, crud.alert_group, TargetTypeEnum.alertgroup, schemas.AlertGroupDetailed, schemas.AlertGroupDetailedCreate)
3131
generic_put(router, crud.alert_group, TargetTypeEnum.alertgroup, schemas.AlertGroupDetailed, schemas.AlertGroupUpdate)
3232
generic_delete(router, crud.alert_group, TargetTypeEnum.alertgroup, schemas.AlertGroupDetailed)
33+
generic_search(router, crud.alert_group, TargetTypeEnum.alertgroup, schemas.AlertGroupSearch, schemas.AlertGroup)
3334
generic_undelete(router, crud.alert_group, TargetTypeEnum.alertgroup, schemas.AlertGroupDetailed)
3435
generic_entities(router, TargetTypeEnum.alertgroup)
3536
generic_history(router, crud.alert_group, TargetTypeEnum.alertgroup)
3637
generic_reflair(router, crud.alert_group, TargetTypeEnum.alertgroup, schemas.AlertGroupDetailed)
37-
generic_search(router, crud.alert_group, TargetTypeEnum.alertgroup, schemas.AlertGroupSearch, schemas.AlertGroup)
38+
generic_export(router, crud.alert_group, TargetTypeEnum.alertgroup)
3839
generic_upvote_and_downvote(router, crud.alert_group, TargetTypeEnum.alertgroup, schemas.AlertGroup)
3940
generic_user_links(router, crud.alert_group, TargetTypeEnum.alertgroup, schemas.AlertGroupDetailed)
41+
generic_tag_untag(router, crud.alert_group, TargetTypeEnum.alertgroup, schemas.AlertGroup)
4042

4143

4244
alertgroup_read_dep = Depends(deps.PermissionCheckId(TargetTypeEnum.alertgroup, PermissionEnum.read))

0 commit comments

Comments
 (0)