Skip to content

Commit 007cc49

Browse files
committed
4.3 Release
1 parent cdb289b commit 007cc49

File tree

198 files changed

+3702
-2696
lines changed

Some content is hidden

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

198 files changed

+3702
-2696
lines changed

conversion/conversion.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
This directory contains scripts for migrating data from version 3 of SCOT to version 4. They are grouped into three categories: database migrations, file migrations, and extra (optional) migrations. Three bash shell scripts have been provided for you to run the applicable migrations in each category.
44

5+
## Necessary tools to run conversion
6+
You will need the following installed to run conversions:
7+
- python 3.11 with the contents of requirements-test.txt from this repo installed via pip
8+
- mysql-shell
9+
510
## Database Conversion
611
This set of scripts migrates the core database data to the SCOT4 database by pulling data directly from the SCOT3 mongodb database. Almost all SCOT3 installations migrating to SCOT4 will want to do this. The `database_conversion.sh` script will run all of the necessary scripts for you.
712

@@ -45,4 +50,4 @@ The following environment variables should be set when running `file_conversion.
4550
- SCOT_ADMIN_APIKEY - a SCOT4 API key with admin priveleges (see above for one way to create one)
4651
- SCOT3_FILE_PREFIX (needed for file migration) - the directory under which the files were stored in the SCOT3 database, this defaults to the default in SCOT3, which was `/opt/scotfiles/`
4752
- SCOT_FILES_DIR (needed for file migration) - the directory on the current machine in which the old SCOT3 files are stored (with the same file structure that the SCOT3 installation had)
48-
- SCOT_CACHED_IMAGES_DIR (needed for cached images migration) - the directory on the current machine that contains the SCOT3 cached images in their original file structure (this is usually the /cached_images/ directory in the SCOT3 files)
53+
- SCOT_CACHED_IMAGES_DIR (needed for cached images migration) - the directory on the current machine that contains the SCOT3 cached images in their original file structure (this is usually the /cached_images/ directory in the SCOT3 files)

conversion/database_migration/bulk_entries.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ def main(mongo_db=None, role_lookup=None):
7373
with tqdm.tqdm(total=scot3_entry_count) as pbar:
7474
bulk_array = []
7575
for entry in scot3_entries:
76+
if entry.get('target') is None or entry['target'].get('type') is None:
77+
pbar.update(1) # Malformed data
78+
continue
7679
if entry['target']['type'] == 'alert' or entry['target']['type'] == 'alertgroup' or entry['target']['type'] == 'feed':
7780
pbar.update(1)
7881
continue

conversion/database_migration/bulk_links.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ def main(mongo_db=None):
1414
scot3_links = mongo_db.link.find()
1515
with tqdm.tqdm(total=scot3_link_count) as pbar:
1616
for link in scot3_links:
17+
if link.get('vertices') is None:
18+
pbar.update(1)
19+
continue
1720
if type(link.get('when')) is int:
1821
new_link = [ datetime.fromtimestamp(link['when']).astimezone(timezone.utc).replace(tzinfo=None), datetime.fromtimestamp(0).astimezone(timezone.utc).replace(tzinfo=None), link['id'], link['vertices'][1]['type'], link['vertices'][1]['id'], link['vertices'][0]['type'], link['vertices'][0]['id']]
1922
else:

conversion/database_migration/bulk_roles.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ def main(mongo_db=None):
2828
for count, role in enumerate(tqdm.tqdm(unique_roles)):
2929
if role == '':
3030
continue
31-
role_row = [count+2, role, 'migrated from SCOT3', datetime.fromtimestamp(0).astimezone(timezone.utc).replace(tzinfo=None), datetime.fromtimestamp(0).astimezone(timezone.utc).replace(tzinfo=None)] #Using an offset of 3 since we have two default groups: admin and everyone, created by default and taking up role ids 1 & 2 respectively
31+
role_row = [count+3, role, 'migrated from SCOT3', datetime.fromtimestamp(0).astimezone(timezone.utc).replace(tzinfo=None), datetime.fromtimestamp(0).astimezone(timezone.utc).replace(tzinfo=None)] #Using an offset of 3 since we have two default groups: admin and everyone, created by default and taking up role ids 1 & 2 respectively
3232
writer.writerow(role_row)
33-
role_lookup[role] = count+2
33+
role_lookup[role] = count+3
3434

3535
return role_lookup
3636

conversion/database_migration/conversion_utilities.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,6 @@ def write_tag_source_links(thing=None, thing_type=None, tag_lookup=None, source_
2222
if thing_type == "signature":
2323
# Check if reference ID exists here
2424
target_data = thing.get('data').get('target')
25-
if target_data is not None:
25+
if target_data is not None and target_data.get('type') is not None and target_data.get('id') is not None:
2626
new_link = [thing_type, thing.get('id'), target_data['type'], target_data['id']]
2727
link_csv_writer.writerow(new_link)

conversion/database_migration/initial_scot4_database.sql

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,10 @@ CREATE TABLE `auth_settings` (
271271
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
272272
/*!40101 SET character_set_client = @saved_cs_client */;
273273

274+
LOCK TABLES `auth_settings` WRITE;
275+
INSERT INTO `auth_settings` VALUES (1, 'local', '{}', 1, '2023-06-06 16:53:27','2023-06-06 16:53:27');
276+
UNLOCK TABLES;
277+
274278
--
275279
-- Table structure for table `auth_storage`
276280
--
@@ -840,9 +844,9 @@ LOCK TABLES `links` WRITE;
840844
/*!40000 ALTER TABLE `links` ENABLE KEYS */;
841845
UNLOCK TABLES;
842846

843-
---
844-
--- Table structure for table `metrics`
845-
---
847+
--
848+
-- Table structure for table `metrics`
849+
--
846850

847851
DROP TABLE IF EXISTS `metrics`;
848852
/*!40101 SET @saved_cs_client = @@character_set_client */;

conversion/database_migration/scot3_scot4_tsv_import.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,4 +74,4 @@
7474
util.import_table(f"{staging_directory}/entity_class_associations.csv", {"table": "entity_class_entity_association", "columns": ['entity_id', 'entity_class_id'], "dialect": "tsv", 'fieldsEscapedBy': '\t', 'fieldsEnclosedBy': "'", 'fieldsEscapedBy': '\0', 'linesTerminatedBy': "\n", "skipRows": 1, "showProgress": True})
7575

7676
if os.path.isfile("./games.csv"):
77-
util.import_table("./games.csv", { "table": "games", "columns": [], "dialect": "tsv", 'fieldsEscapedBy': '\t', 'fieldsEnclosedBy': "'", 'fieldsEscapedBy': '\0', 'linesTerminatedBy': "\n", "skipRows": 1, "showProgress": True})
77+
util.import_table("./games.csv", { "table": "games", "columns": ["game_id", "game_name", "tooltip", "results", "created_date", "modified_date"], "dialect": "tsv", 'fieldsEscapedBy': '\t', 'fieldsEnclosedBy': "'", 'fieldsEscapedBy': '\0', 'linesTerminatedBy': "\n", "skipRows": 1, "showProgress": True})

conversion/extra_migration/update_admin_password_and_api_key.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ def main(db_session):
88
new_admin_apikey = os.getenv('SCOT_ADMIN_APIKEY')
99
db_obj = db_session.query(User).filter(User.username=='scot-admin').one_or_none()
1010
if new_admin_pw:
11-
update_dict={'username':'scot-admin', 'password':os.environ['SCOT_ADMIN_PASSWORD'])
11+
update_dict={'username':'scot-admin', 'password':os.environ['SCOT_ADMIN_PASSWORD']}
1212
crud.user.update(db_session=db_session, db_obj=db_obj, obj_in=update_dict)
1313
else:
1414
print('SCOT_ADMIN_PASSWORD not set, not resetting scot-admin password')

conversion/file_migration/migrate_cached_images.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
import pathlib
1010
import re
1111
import urllib3
12+
13+
from app.db.session import SessionLocal
14+
from app.models import Entry
1215
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
1316

1417
def rewrite_cached_images(html, flaired_html, entry_id):

requirements-test.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ tqdm==4.66.5
77
pymongo==4.8.0
88
splunk-sdk==2.0.2
99
pytest-xdist==3.6.1
10+
Faker==27.0.0
1011
-r requirements.txt

requirements.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
fastapi==0.112.1
22
SQLAlchemy==2.0.32
3+
pydantic_core==2.23.4
34
pydantic_settings==2.4.0
5+
pydantic==2.9.2
46
mysql-connector-python==9.1.0
57
meilisearch==0.31.4
68
msal==1.30.0
79
python-dateutil==2.9.0.post0
810
nh3==0.2.18
9-
Faker==27.0.0
1011
python-jose==3.3.0
1112
loguru==0.7.2
1213
email_validator==2.0.0

src/app/api/__init__.py

Lines changed: 39 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,13 @@
11
from fastapi import FastAPI, Request
22
from fastapi.middleware.cors import CORSMiddleware
3+
from fastapi.openapi.utils import get_openapi
34
from starlette.middleware.base import BaseHTTPMiddleware
45

56
from app.api.endpoints import router
67
from app.core.config import settings
78
from app.db.session import SessionLocal
89

910

10-
def register_cors(app: FastAPI) -> None:
11-
""" """
12-
app.add_middleware(
13-
CORSMiddleware,
14-
allow_origins=["http://localhost:8080", "http://127.0.0.1:8080"], # Remember we need to
15-
allow_origin_regex=r"moz-extension:\/\/.*", # Needed to make the Plugin function properly in Firefox, works fine in Chrome/Edge... no idea why
16-
allow_credentials=True,
17-
allow_methods=["*"],
18-
allow_headers=["*"],
19-
)
20-
21-
2211
async def db_middleware(request: Request, call_next) -> None:
2312
"""
2413
Middleware that wraps each API call in a transaction
@@ -40,11 +29,6 @@ async def db_middleware(request: Request, call_next) -> None:
4029
return response
4130

4231

43-
def register_router(app: FastAPI) -> None:
44-
""" """
45-
app.include_router(router.api_router, prefix=settings.API_V1_STR)
46-
47-
4832
def create_app() -> FastAPI:
4933
"""
5034
:return:
@@ -56,10 +40,46 @@ def create_app() -> FastAPI:
5640
docs_url=settings.DOCS_URL,
5741
openapi_url=settings.OPENAPI_URL,
5842
redoc_url=settings.REDOC_URL,
43+
swagger_ui_parameters={
44+
"filter": True
45+
}
46+
)
47+
48+
app.include_router(router.api_router, prefix=settings.API_V1_STR)
49+
50+
app.add_middleware(
51+
CORSMiddleware,
52+
allow_origins=["http://localhost:8080", "http://127.0.0.1:8080"], # Remember we need to
53+
allow_origin_regex=r"moz-extension:\/\/.*", # Needed to make the Plugin function properly in Firefox, works fine in Chrome/Edge... no idea why
54+
allow_credentials=True,
55+
allow_methods=["*"],
56+
allow_headers=["*"],
5957
)
6058

61-
register_cors(app)
62-
register_router(app)
6359
app.add_middleware(BaseHTTPMiddleware, dispatch=db_middleware)
6460

61+
if not app.openapi_schema:
62+
# will need to update all links for open source stuff
63+
openapi_schema = get_openapi(
64+
title="SCOT",
65+
version="4.2.0",
66+
summary="Sandia Cyber Omni Tracker Server",
67+
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.",
68+
routes=app.routes,
69+
# terms_of_service?
70+
contact={
71+
"name": "SCOT Development Team",
72+
"url": "http://getscot.sandia.gov",
73+
"email": "scot-dev@sandia.gov"
74+
},
75+
license_info={
76+
"name": "LICENSE",
77+
"identifier": "Apache-2.0",
78+
},
79+
)
80+
openapi_schema["info"]["x-logo"] = {
81+
"url": "https://sandialabs.github.io/scot4-docs/images/scot.png"
82+
}
83+
app.openapi_schema = openapi_schema
84+
6585
return app

src/app/api/deps.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ def get_current_active_user(
9696
if not crud.user.is_active(current_user):
9797
raise HTTPException(status_code=400, detail="Inactive user")
9898
crud.user.update_last_activity(db, current_user)
99+
# add user id to the session for some queries
100+
db.info["user_id"] = current_user.id
99101
db.commit() # Done here to prevent race conditions
100102
return current_user
101103

src/app/api/endpoints/alert.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,25 @@
1616
generic_post,
1717
generic_undelete,
1818
generic_history,
19-
generic_search
19+
generic_search,
20+
generic_upvote_and_downvote,
2021
)
2122

2223
router = APIRouter()
23-
description, examples = create_schema_details(schemas.AlertUpdate)
2424

2525
# Create get, post, put, and delete endpoints
2626
generic_get(router, crud.alert, TargetTypeEnum.alert, schemas.Alert)
27-
generic_post(
28-
router, crud.alert, TargetTypeEnum.alert, schemas.Alert, schemas.AlertCreate
29-
)
27+
generic_post(router, crud.alert, TargetTypeEnum.alert, schemas.Alert, schemas.AlertCreate)
3028
generic_delete(router, crud.alert, TargetTypeEnum.alert, schemas.Alert)
3129
generic_undelete(router, crud.alert, TargetTypeEnum.alert, schemas.Alert)
3230
generic_entries(router, TargetTypeEnum.alert)
3331
generic_entities(router, TargetTypeEnum.alert)
3432
generic_search(router, crud.alert, TargetTypeEnum.alert, schemas.AlertSearch, schemas.Alert)
3533
generic_history(router, crud.alert, TargetTypeEnum.alert)
34+
generic_upvote_and_downvote(router, crud.alert, TargetTypeEnum.alert, schemas.Alert)
35+
36+
37+
description, examples = create_schema_details(schemas.AlertUpdate)
3638

3739

3840
# Custom PUT so that you can modify alerts if you have access to the alertgroup
@@ -71,9 +73,7 @@ def update_alert(
7173
raise HTTPException(422, f"Validation error: {e}")
7274

7375
try:
74-
updated = crud.alert.update(
75-
db_session=db, db_obj=_obj, obj_in=obj, audit_logger=audit_logger
76-
)
76+
updated = crud.alert.update(db, _obj, obj, audit_logger)
7777
except ValueError as e:
7878
raise HTTPException(422, f"Error when updating alert: {e}")
7979

src/app/api/endpoints/alertgroup.py

Lines changed: 22 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -17,52 +17,30 @@
1717
generic_history,
1818
generic_reflair,
1919
generic_search,
20-
generic_export
20+
generic_export,
21+
generic_upvote_and_downvote,
22+
generic_user_links
2123
)
2224

2325
router = APIRouter()
24-
description, examples = create_schema_details(schemas.AlertAdd)
2526

2627
# Create get, post, put, and delete endpoints
2728
generic_export(router, crud.alert_group, TargetTypeEnum.alertgroup)
28-
generic_get(
29-
router, crud.alert_group, TargetTypeEnum.alertgroup, schemas.AlertGroupDetailed
30-
)
31-
32-
generic_post(
33-
router,
34-
crud.alert_group,
35-
TargetTypeEnum.alertgroup,
36-
schemas.AlertGroupDetailed,
37-
schemas.AlertGroupDetailedCreate,
38-
)
39-
generic_put(
40-
router,
41-
crud.alert_group,
42-
TargetTypeEnum.alertgroup,
43-
schemas.AlertGroupDetailed,
44-
schemas.AlertGroupUpdate,
45-
)
46-
generic_delete(
47-
router, crud.alert_group, TargetTypeEnum.alertgroup, schemas.AlertGroupDetailed
48-
)
49-
generic_undelete(
50-
router, crud.alert_group, TargetTypeEnum.alertgroup, schemas.AlertGroupDetailed
51-
)
29+
generic_get(router, crud.alert_group, TargetTypeEnum.alertgroup, schemas.AlertGroupDetailed)
30+
generic_post(router, crud.alert_group, TargetTypeEnum.alertgroup, schemas.AlertGroupDetailed, schemas.AlertGroupDetailedCreate)
31+
generic_put(router, crud.alert_group, TargetTypeEnum.alertgroup, schemas.AlertGroupDetailed, schemas.AlertGroupUpdate)
32+
generic_delete(router, crud.alert_group, TargetTypeEnum.alertgroup, schemas.AlertGroupDetailed)
33+
generic_undelete(router, crud.alert_group, TargetTypeEnum.alertgroup, schemas.AlertGroupDetailed)
5234
generic_entities(router, TargetTypeEnum.alertgroup)
5335
generic_history(router, crud.alert_group, TargetTypeEnum.alertgroup)
54-
generic_reflair(
55-
router, crud.alert_group, TargetTypeEnum.alertgroup, schemas.AlertGroupDetailed
56-
)
36+
generic_reflair(router, crud.alert_group, TargetTypeEnum.alertgroup, schemas.AlertGroupDetailed)
5737
generic_search(router, crud.alert_group, TargetTypeEnum.alertgroup, schemas.AlertGroupSearch, schemas.AlertGroup)
38+
generic_upvote_and_downvote(router, crud.alert_group, TargetTypeEnum.alertgroup, schemas.AlertGroup)
39+
generic_user_links(router, crud.alert_group, TargetTypeEnum.alertgroup, schemas.AlertGroupDetailed)
5840

5941

60-
alertgroup_read_dep = Depends(
61-
deps.PermissionCheckId(TargetTypeEnum.alertgroup, PermissionEnum.read)
62-
)
63-
alertgroup_modify_dep = Depends(
64-
deps.PermissionCheckId(TargetTypeEnum.alertgroup, PermissionEnum.modify)
65-
)
42+
alertgroup_read_dep = Depends(deps.PermissionCheckId(TargetTypeEnum.alertgroup, PermissionEnum.read))
43+
alertgroup_modify_dep = Depends(deps.PermissionCheckId(TargetTypeEnum.alertgroup, PermissionEnum.modify))
6644

6745

6846
@router.get(
@@ -75,18 +53,20 @@ def read_alertgroup_alerts(
7553
*,
7654
db: Session = Depends(deps.get_db),
7755
id: Annotated[int, Path(...)],
78-
current_user: models.User = Depends(deps.get_current_active_user),
7956
audit_logger: deps.AuditLogger = Depends(deps.get_audit_logger),
8057
) -> Any:
8158
"""
8259
Get all alerts for this alertgroup
8360
"""
84-
_alert_group = crud.alert_group.get(db_session=db, _id=id, audit_logger=audit_logger)
61+
_alert_group = crud.alert_group.get(db, id, audit_logger)
8562
if not _alert_group:
8663
raise HTTPException(404, "Alert with id %s not found" % id)
8764
return _alert_group.alerts
8865

8966

67+
description, examples = create_schema_details(schemas.AlertAdd)
68+
69+
9070
@router.post(
9171
"/{id}/alerts",
9272
response_model=schemas.Alert,
@@ -99,16 +79,15 @@ def add_alertgroup_alert(
9979
db: Session = Depends(deps.get_db),
10080
id: Annotated[int, Path(...)],
10181
alert: Annotated[schemas.AlertAdd, Body(..., openapi_examples=examples)],
102-
current_user: models.User = Depends(deps.get_current_active_user),
82+
_: models.User = Depends(deps.get_current_active_user),
10383
audit_logger: deps.AuditLogger = Depends(deps.get_audit_logger),
10484
) -> Any:
10585
"""
10686
Add an alert to an alertgroup
10787
"""
10888
try:
10989
alert.alertgroup_id = id
110-
_alert = crud.alert.add_to_alert_group(db_session=db, obj_in=alert, audit_logger=audit_logger)
111-
return _alert
90+
return crud.alert.add_to_alert_group(db_session=db, obj_in=alert, audit_logger=audit_logger)
11291
except Exception as e:
11392
raise HTTPException(status_code=422, detail=str(e))
11493

@@ -124,6 +103,7 @@ def add_alertgroup_column(
124103
db: Session = Depends(deps.get_db),
125104
id: int,
126105
column_name: str = Body(...),
127-
values: Dict[str, str] = Body({})
106+
values: Dict[str, str] = Body({}),
107+
current_user: models.User = Depends(deps.get_current_active_user)
128108
) -> Any:
129-
return crud.alert_group.add_column(db, id, column_name, values)
109+
return crud.alert_group.add_column(db, id, column_name, values, current_user=current_user)

0 commit comments

Comments
 (0)