Skip to content

Commit f9d60d8

Browse files
author
Janson Bunce
committed
Resolve merge conflicts
2 parents 818c5e9 + 3f8672a commit f9d60d8

File tree

15 files changed

+1150
-1248
lines changed

15 files changed

+1150
-1248
lines changed

backend/src/xfd_django/xfd_api/api_methods/cve.py

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,15 @@
11
"""Cve API."""
2+
# Standard Python Libraries
3+
import datetime
4+
from typing import Optional
25

36
# Third-Party Libraries
4-
from fastapi import HTTPException
5-
from xfd_mini_dl.models import Cve
7+
from django.core.paginator import EmptyPage, PageNotAnInteger, Paginator
8+
from django.db.models import Q
9+
from fastapi import HTTPException, status
10+
from xfd_mini_dl.models import Cve as CveModel
11+
12+
from ..auth import is_global_write_admin
613

714

815
def get_cves_by_id(cve_id):
@@ -13,7 +20,7 @@ def get_cves_by_id(cve_id):
1320
object: a single Cve object.
1421
"""
1522
try:
16-
cve = Cve.objects.get(id=cve_id)
23+
cve = CveModel.objects.get(id=cve_id)
1724
return cve
1825
except Exception as e:
1926
raise HTTPException(status_code=500, detail=str(e))
@@ -27,7 +34,56 @@ def get_cves_by_name(cve_name):
2734
object: a single Cpe object.
2835
"""
2936
try:
30-
cve = Cve.objects.get(name=cve_name)
37+
cve = CveModel.objects.get(name=cve_name)
3138
return cve
3239
except Exception as e:
3340
raise HTTPException(status_code=500, detail=str(e))
41+
42+
43+
async def get_all_cves(
44+
current_user,
45+
*,
46+
page: int = 1,
47+
per_page: int = 100,
48+
since_timestamp: Optional[datetime.datetime] = None,
49+
) -> tuple[int, list[CveModel]]:
50+
"""
51+
Return (total_pages, list_of_CveModel) for the given filters.
52+
53+
Raise HTTPException(403) if the user is not an admin, or HTTPException(500) on DB errors.
54+
"""
55+
if not is_global_write_admin(current_user):
56+
raise HTTPException(
57+
status_code=status.HTTP_403_FORBIDDEN,
58+
detail="Unauthorized access.",
59+
)
60+
61+
try:
62+
# 1) base queryset
63+
qs = CveModel.objects.all()
64+
65+
# 2) optional date filter
66+
if since_timestamp is not None:
67+
qs = qs.filter(Q(modified_at__gte=since_timestamp))
68+
69+
# 3) deterministic ordering
70+
qs = qs.order_by("modified_at", "id")
71+
72+
# 4) paginate
73+
paginator = Paginator(qs, per_page)
74+
try:
75+
page_obj = paginator.page(page)
76+
objects = list(page_obj.object_list)
77+
except PageNotAnInteger:
78+
page_obj = paginator.page(1)
79+
objects = list(page_obj.object_list)
80+
except EmptyPage:
81+
objects = []
82+
83+
return paginator.num_pages, objects
84+
85+
except Exception as e:
86+
raise HTTPException(
87+
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
88+
detail=f"DB error: {e}",
89+
)
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
"""Get CVEs from FastAPI endpoint and sync to local DB."""
2+
# Third-Party Libraries
3+
from django.core.management.base import BaseCommand
4+
from xfd_api.tasks.nist_lz_sync import handler
5+
6+
7+
class Command(BaseCommand):
8+
"""Sync CVEs from FastAPI endpoint to local DB."""
9+
10+
help = "Fetch CVEs from the FastAPI endpoint and sync to local DB"
11+
12+
def handle(self, *args, **options):
13+
"""Handle the command."""
14+
result = handler()
15+
self.stdout.write(str(result))

backend/src/xfd_django/xfd_api/schema_models/cve.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,10 @@ class Config:
4444
"""Config."""
4545

4646
from_attributes = True
47+
48+
49+
class GetAllCvesResponse(BaseModel):
50+
"""GetAllCvesResponse schema."""
51+
52+
status: str
53+
payload: List[Cve]

backend/src/xfd_django/xfd_api/schema_models/scan.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,14 @@ class GenericMessageResponseModel(BaseModel):
253253
global_scan=True,
254254
description="Update CVE data using the NIST API",
255255
),
256+
"nist_lz_sync": ScanSchema(
257+
type="fargate",
258+
is_passive=True,
259+
global_scan=True,
260+
cpu="1024",
261+
memory="8192",
262+
description="Pull in NIST cve data from commercial mdl",
263+
),
256264
"portscanner": ScanSchema(
257265
type="fargate",
258266
is_passive=False,
@@ -376,10 +384,11 @@ class GenericMessageResponseModel(BaseModel):
376384
"xpanse_alert_pull": ScanSchema(
377385
type="fargate",
378386
is_passive=True,
379-
global_scan=True,
387+
global_scan=False,
380388
cpu="1024",
381389
memory="8192",
382390
description="Pull in Xpanse alert data from Xpanse API",
391+
max_concurrent_tasks=3,
383392
),
384393
"xpanse_org_sync": ScanSchema(
385394
type="fargate",

backend/src/xfd_django/xfd_api/tasks/ecs_client.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ def run_command(self, command_options):
5858
container = self.docker.containers.run(
5959
"crossfeed-worker",
6060
name=container_name,
61-
network_mode="xfd_backend",
61+
network_mode="backend",
6262
mem_limit="4g",
6363
environment={
6464
"CROSSFEED_COMMAND_OPTIONS": json.dumps(command_options),
@@ -106,6 +106,7 @@ def run_command(self, command_options):
106106
"SERVICE_QUEUE_URL": os.getenv("QUEUE_URL", ""),
107107
"DMZ_SYNC_ENDPOINT": os.getenv("DMZ_SYNC_ENDPOINT", ""),
108108
"DMZ_API_KEY": os.getenv("DMZ_API_KEY", ""),
109+
"QUEUE_URL": os.getenv("QUEUE_URL", ""),
109110
"XPANSE_ORG_SYNC_BUCKET_NAME": os.getenv(
110111
"XPANSE_ORG_SYNC_BUCKET_NAME"
111112
),
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
"""CVE Sync scan hitting new FastAPI endpoint."""
2+
# --- Standard Libraries ---
3+
# Standard Python Libraries
4+
from datetime import datetime, timezone
5+
import hashlib
6+
import json
7+
import logging
8+
import os
9+
from urllib.parse import urljoin
10+
11+
# Third-Party Libraries
12+
# --- Third-Party Libraries ---
13+
import django
14+
import requests
15+
16+
# --- Django setup ---
17+
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "xfd_django.settings")
18+
os.environ["DJANGO_ALLOW_ASYNC_UNSAFE"] = "true"
19+
django.setup()
20+
21+
# Third-Party Libraries
22+
from xfd_api.helpers.date_time_helpers import calculate_days_back
23+
24+
# --- Your CVE model import ---
25+
from xfd_mini_dl.models import Cve as CveModel
26+
from xfd_mini_dl.models import DataSource
27+
28+
# --- Constants & Logging ---
29+
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
30+
LOGGER = logging.getLogger(__name__)
31+
32+
SALT = os.getenv("CHECKSUM_SALT", "default_salt")
33+
HEADERS = {
34+
"X-API-KEY": os.getenv("DMZ_API_KEY"),
35+
"Content-Type": "application/json",
36+
}
37+
38+
# e.g. “https://api.staging-cd.crossfeed.cyber.dhs.gov/sync”
39+
base_url = os.getenv("DMZ_SYNC_ENDPOINT", "").rstrip("/")
40+
41+
if base_url.endswith("/sync"):
42+
# drop “/sync” and append “/cves”
43+
CVE_API_URL = base_url.rsplit("/", 1)[0] + "/dmz_sync/cves"
44+
else:
45+
# fallback: join “/cves” onto whatever they provided
46+
CVE_API_URL = urljoin(base_url + "/", "dmz_sync/cves")
47+
48+
49+
def validate_response_checksum(response):
50+
"""Validate the checksum from the API."""
51+
try:
52+
data = response.json()
53+
received = response.headers.get("X-Salted-Checksum")
54+
if not received:
55+
LOGGER.warning("No checksum header")
56+
return False
57+
58+
serialized = json.dumps(data, default=str, sort_keys=True)
59+
calc = hashlib.sha256((SALT + serialized).encode()).hexdigest()
60+
return received == calc
61+
except Exception as e:
62+
LOGGER.error("Error validating checksum: %s", e)
63+
return False
64+
65+
66+
def save_cves_to_db(cve_list):
67+
"""
68+
Upsert each CVE dict into the local DB.
69+
70+
Matches your corrected Cve model with ArrayFields.
71+
"""
72+
for item in cve_list:
73+
# parse ISO timestamps
74+
try:
75+
pub = datetime.datetime.fromisoformat(item["published_at"])
76+
except Exception:
77+
pub = None
78+
try:
79+
mod = datetime.datetime.fromisoformat(item["modified_at"])
80+
except Exception:
81+
mod = None
82+
83+
defaults = {
84+
"name": item.get("name"),
85+
"published_at": pub,
86+
"modified_at": mod,
87+
"status": item.get("status"),
88+
"description": item.get("description"),
89+
# CVSS v2
90+
"cvss_v2_source": item.get("cvss_v2_source"),
91+
"cvss_v2_type": item.get("cvss_v2_type"),
92+
"cvss_v2_version": item.get("cvss_v2_version"),
93+
"cvss_v2_vector_string": item.get("cvss_v2_vector_string"),
94+
"cvss_v2_base_score": item.get("cvss_v2_base_score"),
95+
"cvss_v2_base_severity": item.get("cvss_v2_base_severity"),
96+
"cvss_v2_exploitability_score": item.get("cvss_v2_exploitability_score"),
97+
"cvss_v2_impact_score": item.get("cvss_v2_impact_score"),
98+
# CVSS v3
99+
"cvss_v3_source": item.get("cvss_v3_source"),
100+
"cvss_v3_type": item.get("cvss_v3_type"),
101+
"cvss_v3_version": item.get("cvss_v3_version"),
102+
"cvss_v3_vector_string": item.get("cvss_v3_vector_string"),
103+
"cvss_v3_base_score": item.get("cvss_v3_base_score"),
104+
"cvss_v3_base_severity": item.get("cvss_v3_base_severity"),
105+
"cvss_v3_exploitability_score": item.get("cvss_v3_exploitability_score"),
106+
"cvss_v3_impact_score": item.get("cvss_v3_impact_score"),
107+
# CVSS v4
108+
"cvss_v4_source": item.get("cvss_v4_source"),
109+
"cvss_v4_type": item.get("cvss_v4_type"),
110+
"cvss_v4_version": item.get("cvss_v4_version"),
111+
"cvss_v4_vector_string": item.get("cvss_v4_vector_string"),
112+
"cvss_v4_base_score": item.get("cvss_v4_base_score"),
113+
"cvss_v4_base_severity": item.get("cvss_v4_base_severity"),
114+
"cvss_v4_exploitability_score": item.get("cvss_v4_exploitability_score"),
115+
"cvss_v4_impact_score": item.get("cvss_v4_impact_score"),
116+
# ArrayFields
117+
"weaknesses": item.get("weaknesses"),
118+
"reference_urls": item.get("reference_urls"),
119+
"cpe_list": item.get("cpe_list"),
120+
# dve_score left untouched (payload doesn’t include it)
121+
}
122+
123+
try:
124+
CveModel.objects.update_or_create(id=item["id"], defaults=defaults)
125+
except Exception as e:
126+
LOGGER.error("Error saving CVE %s: %s", item["id"], e)
127+
128+
129+
def handler(command_options=None):
130+
"""Fetch all CVEs and save them locally."""
131+
nist_source, _ = DataSource.objects.get_or_create(
132+
name="Nist",
133+
defaults={
134+
"description": "Nist cve capture",
135+
"last_run": timezone.now().date(),
136+
},
137+
)
138+
139+
since_date = calculate_days_back(15)
140+
141+
try:
142+
LOGGER.info("Starting CVE sync…")
143+
page = 1
144+
per_page = 200
145+
done = False
146+
147+
while not done:
148+
payload = {
149+
"page": page,
150+
"page_size": per_page,
151+
"since_date": since_date,
152+
}
153+
154+
resp = requests.post(CVE_API_URL, headers=HEADERS, json=payload, timeout=60)
155+
resp.raise_for_status()
156+
157+
if not validate_response_checksum(resp):
158+
LOGGER.error("Checksum mismatch!")
159+
return {"statusCode": 500, "body": "Checksum mismatch"}
160+
161+
body = resp.json()
162+
if body.get("status") != "ok":
163+
LOGGER.error("API returned bad status: %s", body)
164+
return {"statusCode": 500, "body": "Bad status"}
165+
total_pages = resp.get("total_pages", 1)
166+
current_page = resp.get("current_page", 1)
167+
payload = body.get("payload", [])
168+
LOGGER.info("Fetched %s CVEs", len(payload))
169+
save_cves_to_db(payload)
170+
if current_page >= total_pages:
171+
done = True
172+
else:
173+
page += 1
174+
LOGGER.info("CVE sync completed successfully")
175+
176+
return {"statusCode": 200, "body": "Shodan sync completed successfully."}
177+
178+
except Exception as e:
179+
LOGGER.error("Sync error: %s", e)
180+
return {"statusCode": 500, "body": str(e)}

backend/src/xfd_django/xfd_api/tasks/syncdb_helpers.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -621,7 +621,11 @@ def create_api_key_for_user(user):
621621
)
622622

623623
# Print the raw key for debugging or manual testing
624-
print("Created API key for user {}: {}".format(user.email, key))
624+
print(
625+
"Created API key for user, keep this and enter at .env file CF_API_KEY {}: {}".format(
626+
user.email, key
627+
)
628+
)
625629

626630

627631
def generate_random_name():

0 commit comments

Comments
 (0)