Skip to content

Commit ec641c4

Browse files
add database more info
1 parent b6b0250 commit ec641c4

18 files changed

+371
-54
lines changed

.env.template .env.docker.template

+3-1
Original file line numberDiff line numberDiff line change
@@ -11,4 +11,6 @@ POSTGRES_PASSWORD=postgres
1111
ENVIRONMENT=development
1212

1313
# Note there maybe be some confusion with urls, use named services instead of
14-
# localhost inside docker-compose.yml for containers to communicate with each other.
14+
# localhost inside docker-compose.yml for containers to communicate with each other.
15+
16+

.gitignore

+10
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,16 @@
99
.idea/**/dictionaries
1010
.idea/**/shelf
1111

12+
# CVE Search plugin data
13+
14+
.cve_search_data/
15+
16+
.DS_Store
17+
18+
.env.docker
19+
20+
cve-search
21+
1222
# AWS User-specific
1323
.idea/**/aws.xml
1424

Makefile

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
11

2+
.ONESHELL: # Applies to every targets in the file!
3+
4+
all:
5+
cd ~/some_dir
6+
pwd # Prints ~/some_dir if cd succeeded
7+
28

39
db-schema:
410
ifeq ($(name),)
@@ -17,4 +23,4 @@ db-migrate-down-base:
1723

1824

1925
dev:
20-
cd src && uvicorn app:app --host 0.0.0.0 --port 8001 --reload
26+
cd src && uvicorn app:app --host 0.0.0.0 --port 8001 --reload

docker-compose.yml

+20-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ services:
88
depends_on:
99
- db
1010
env_file:
11-
- .env
11+
- .env.docker
1212
develop:
1313
watch:
1414
- action: sync+restart
@@ -56,7 +56,26 @@ services:
5656
dockerfile: ./dashboard/Dockerfile
5757
ports:
5858
- "3000:3000"
59+
60+
61+
# cve_search:
62+
# extends:
63+
# file: cve-search/docker-compose.yml
64+
# service: cve_search
65+
# environment:
66+
# - NVD_NIST_API_KEY=${NVD_NIST_API_KEY}
67+
# mongo:
68+
# extends:
69+
# file: cve-search/docker-compose.yml
70+
# service: mongo
71+
# redis:
72+
# extends:
73+
# file: cve-search/docker-compose.yml
74+
# service: redis
5975
volumes:
6076
ollama:
6177
db:
6278
dashboard:
79+
80+
# networks:
81+
# cve_search:

requirements.txt

+3-4
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,14 @@
1-
python-dotenv~=0.19.1
2-
pydantic~=2.7.1
1+
pydantic
32
sqlmodel~=0.0.18
43
alembic~=1.13.1
54
psycopg
65
psycopg-binary
7-
fastapi==0.68.1
6+
fastapi
87
uvicorn==0.15.0
98
python-dotenv==0.19.1
109
jsonschema==4.22.0
1110
requests~=2.31.0
1211
SQLAlchemy~=2.0.30
1312
tqdm~=4.66.4
1413
httpx~=0.27.0
15-
tenacity~=8.3.0
14+
tenacity~=8.3.0

run

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#!/bin/bash
2+
3+
source venv/bin/activate
4+
uvicorn src.app:app --host 0.0.0.0 --port 8000

src/app.py

+53-34
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,23 @@
1+
import datetime
12
import time
2-
3-
from fastapi import FastAPI, Request
3+
from typing import Annotated
4+
from fastapi import Body, FastAPI,Response,Query
45
from fastapi.middleware.cors import CORSMiddleware
56

67
import api.ollama as ollama
78
from data.helper import get_content_list
8-
from data.db_types import Recommendation, Response
9+
from data.types import Recommendation, Response,Content
910
from my_db import Session
10-
from models.models import Finding
11-
from models.models import Recommendation as DBRecommendation
11+
import models.models as db_models
12+
1213

13-
# from data.Findings import Findings
14+
import data.apischema as apischema
1415

16+
from sqlalchemy import Date, cast
1517

1618
app = FastAPI()
1719

20+
1821
app.add_middleware(
1922
CORSMiddleware,
2023
allow_origins=['*'],
@@ -47,42 +50,51 @@ def health():
4750

4851

4952
@app.post('/upload')
50-
async def upload(request: Request, response: Response):
53+
async def upload(data: Annotated[apischema.StartRecommendationTaskRequest,Body(...)]):
5154
"""
5255
This function takes the string from the request and converts it to a data object.
5356
:return: 200 OK if the data is valid, 400 BAD REQUEST otherwise.
5457
"""
5558

56-
try:
57-
json_data = await request.json()
58-
except Exception as e:
59-
response.status_code = 400
60-
return 'Invalid JSON data'
61-
62-
# Check if the JSON data is valid
63-
# TODO: fix required properties for eg cvss_rating_list is not required
64-
# if not validate_json(json_data):
65-
# return 'Invalid JSON data', 400
66-
67-
# Convert into Response object
68-
response = Response.validate(json_data)
69-
7059
# get the content list
71-
content_list = get_content_list(response)
60+
content_list = get_content_list(data.data)
7261

7362
with Session() as s:
74-
for c in content_list:
75-
find = Finding(finding=c.title_list[0].element, content=c.json())
76-
s.add(find)
77-
s.commit()
63+
today = datetime.datetime.now().date()
64+
existing_task = s.query(db_models.RecommendationTask).filter(cast(db_models.RecommendationTask.created_at,Date)== today).order_by(db_models.RecommendationTask.created_at).first()
65+
if existing_task and data.force_update is False:
66+
67+
return 'Recommendation task already exists for today', 400
68+
69+
if(data.force_update):
70+
# Will nuke old task with all its findings.
71+
if(existing_task.status == db_models.TaskStatus.PENDING):
72+
return 'Recommendation task is already processing, cannot exit', 400
73+
74+
s.query(db_models.RecommendationTask).filter(db_models.RecommendationTask.id == existing_task.id).delete()
75+
s.commit()
76+
s.delete(existing_task)
77+
s.commit()
78+
79+
recommendation_task = db_models.RecommendationTask()
80+
81+
s.add(recommendation_task)
82+
s.commit()
83+
s.flush()
84+
s.refresh(recommendation_task)
85+
for c in content_list:
86+
find = db_models.Finding().from_data(c)
87+
find.recommendation_task_id = recommendation_task.id
88+
s.add(find)
89+
s.commit()
7890
# start subprocess for processing the data
7991
# ...
8092

8193
return 'Data uploaded successfully'
8294

8395

8496
@app.get('/recommendations')
85-
def recommendations():
97+
def recommendations(request: Annotated[apischema.GetRecommendationRequest,Query(...)]):
8698
"""
8799
This function returns the recommendations from the data.
88100
:return: 200 OK with the recommendations or 204 NO CONTENT if there are no recommendations with retry-after header.
@@ -91,13 +103,20 @@ def recommendations():
91103
# get the findings
92104
# ...
93105
with Session() as s:
94-
recs = s.query(DBRecommendation).all()
95-
96-
recommendations = [Recommendation(recommendation='aa', generic=True) for r in recs]
97-
# get the recommendations
98-
99-
if recommendations:
100-
return recommendations, 200
106+
total_count = s.query(db_models.Finding).count()
107+
recs = s.query(db_models.Finding).join(db_models.RecommendationTask).offset(request.pagination.offset).limit(request.pagination.limit).all()
108+
findings = apischema.GetRecommendationResponse(
109+
items=[apischema.GetRecommendationResponseItem(
110+
description_short="this is a short description",
111+
description_long="this is a long description",
112+
finding='finding'
113+
114+
) for r in recs],
115+
pagination=apischema.Pagination(offset=request.pagination.offset, limit=request.pagination.limit,total=total_count )
116+
)
117+
118+
if findings:
119+
return findings, 200
101120
else:
102121
return 'No recommendations found', 204, {'Retry-After': 60}
103122

src/data/apischema.py

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
2+
3+
4+
5+
from pydantic import BaseModel
6+
from typing import Optional
7+
8+
from data.pagination import Pagination,PaginationInput
9+
from data.types import Recommendation, Content,Response
10+
11+
12+
class StartRecommendationTaskRequest(BaseModel):
13+
user_id: int
14+
data: Response
15+
force_update: Optional[bool] = False
16+
17+
class StartRecommendationTaskResponse(BaseModel):
18+
task_id: int
19+
20+
21+
class GetRecommendationFilter(BaseModel):
22+
location:Optional[str]
23+
severity: Optional[str]
24+
cve_id: Optional[str]
25+
source: Optional[str]
26+
27+
28+
class GetRecommendationRequest(BaseModel):
29+
user_id: int
30+
filter: Optional[GetRecommendationFilter]
31+
pagination: Optional[PaginationInput] = PaginationInput(offset=0, limit=10)
32+
33+
34+
35+
class GetRecommendationResponseItem(BaseModel):
36+
description_short: str
37+
description_long: str
38+
finding: str
39+
40+
class GetRecommendationResponse(BaseModel):
41+
items: list[GetRecommendationResponseItem]
42+
pagination: Pagination
43+
44+
45+
class GetSummarizedRecommendationRequest(BaseModel):
46+
user_id: int
47+
pagination: PaginationInput = PaginationInput(offset=0, limit=10)
48+
49+
50+
class GetSummarizedRecommendationResponse(BaseModel):
51+
recommendation: list[Recommendation]
52+
pagination: Pagination
53+
54+

src/data/helper.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import jsonschema
33
from jsonschema import validate
44

5-
from .db_types import schema, Response, Content
5+
from .types import schema, Response, Content
66

77
##TODO:maybe using pydantic should be enough
88
def validate_json(data: any) -> bool:

src/data/db_types.py src/data/types.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,10 @@ class Rule(BaseModel):
116116

117117

118118
class CveId(BaseModel):
119+
element: str
120+
source: str
121+
122+
class CweId(BaseModel):
119123
element: List[str]
120124
source: str
121125

@@ -139,7 +143,8 @@ class Content(BaseModel):
139143
internal_ratingsource_list: List[Rating]
140144
cvss_rating_list: Optional[ List[CvssRating]] = None
141145
rule_list: List[Rule]
142-
cwe_id_list: Optional[ List[CveId]] = None
146+
cve_id_list: Optional[ List[CveId]] = None
147+
cwe_id_list: Optional[ List[CweId]] = None
143148
activity_list: List[Activity]
144149
first_found: str
145150
last_found: str

src/migrations/env.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
# target_metadata = None
2121

2222
from models.base import Base
23-
23+
import models
2424
from models.models import *
2525

2626
target_metadata = Base.metadata
@@ -36,7 +36,7 @@ def get_url():
3636
# load env from file
3737

3838

39-
from db import (get_db_url)
39+
from my_db import (get_db_url)
4040
return get_db_url()
4141

4242

src/migrations/script.py.mako

+1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ Create Date: ${create_date}
88
from alembic import op
99
import sqlalchemy as sa
1010
import sqlmodel.sql.sqltypes
11+
import models
1112
${imports if imports else ""}
1213

1314
# revision identifiers, used by Alembic.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
"""add_more_tables2
2+
3+
Revision ID: c3fdb30822c4
4+
Revises: cefb826a201b
5+
Create Date: 2024-06-11 17:59:48.102377
6+
7+
"""
8+
from alembic import op
9+
import sqlalchemy as sa
10+
import sqlmodel.sql.sqltypes
11+
import models
12+
13+
14+
# revision identifiers, used by Alembic.
15+
revision = 'c3fdb30822c4'
16+
down_revision = 'cefb826a201b'
17+
branch_labels = None
18+
depends_on = None
19+
20+
21+
def upgrade():
22+
# ### commands auto generated by Alembic - please adjust! ###
23+
op.alter_column('findings', 'recommendation_task_id',
24+
existing_type=sa.INTEGER(),
25+
nullable=False)
26+
op.drop_constraint('findings_recommendation_task_id_fkey', 'findings', type_='foreignkey')
27+
op.create_foreign_key(None, 'findings', 'recommendation_task', ['recommendation_task_id'], ['id'], ondelete='CASCADE')
28+
op.add_column('recommendations', sa.Column('recommendation_task_id', sa.Integer(), nullable=False))
29+
op.create_foreign_key(None, 'recommendations', 'recommendation_task', ['recommendation_task_id'], ['id'], ondelete='CASCADE')
30+
# ### end Alembic commands ###
31+
32+
33+
def downgrade():
34+
# ### commands auto generated by Alembic - please adjust! ###
35+
op.drop_constraint(None, 'recommendations', type_='foreignkey')
36+
op.drop_column('recommendations', 'recommendation_task_id')
37+
op.drop_constraint(None, 'findings', type_='foreignkey')
38+
op.create_foreign_key('findings_recommendation_task_id_fkey', 'findings', 'recommendation_task', ['recommendation_task_id'], ['id'])
39+
op.alter_column('findings', 'recommendation_task_id',
40+
existing_type=sa.INTEGER(),
41+
nullable=True)
42+
# ### end Alembic commands ###

0 commit comments

Comments
 (0)