Skip to content

Commit 29a35c0

Browse files
authored
Merge pull request #37 from ozanunal0/dev-clean
Fix EXA AI results and semantic output fields were not showing up in the Detailed Vulnerability Analysis page
2 parents eb0d4c9 + e4b6fb8 commit 29a35c0

4 files changed

Lines changed: 226 additions & 3 deletions

File tree

src/clients/exa_client.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,3 +449,13 @@ async def find_similar_threat_articles(
449449
logger.error(f"Error finding similar articles for '{reference_url}': {str(e)}")
450450
# Re-raise to trigger retry mechanism
451451
raise
452+
453+
454+
def is_exa_client_available() -> bool:
455+
"""
456+
Checks if the EXA client was successfully initialized.
457+
458+
Returns:
459+
bool: True if the client is initialized and ready, False otherwise.
460+
"""
461+
return exa is not None

src/dashboard/pages/02_Detailed_Analysis.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,8 +117,17 @@ def get_search_results(cve_id=None, keyword=None, priorities=None, is_in_kev=Non
117117
# Get a list of CVE IDs for selection
118118
cve_list = df["cve_id"].tolist()
119119

120+
# Check if a specific CVE was requested via URL parameter
121+
query_params = st.query_params
122+
requested_cve = query_params.get("selected_cve")
123+
124+
# Determine the default index for the selectbox
125+
default_index = 0
126+
if requested_cve and requested_cve in cve_list:
127+
default_index = cve_list.index(requested_cve)
128+
120129
# Right-side selection box
121-
selected_cve = st.selectbox("Select a vulnerability for detailed analysis:", options=cve_list, index=0)
130+
selected_cve = st.selectbox("Select a vulnerability for detailed analysis:", options=cve_list, index=default_index)
122131

123132
# Get the selected CVE data
124133
selected_data = df[df["cve_id"] == selected_cve].iloc[0].to_dict()

src/dashboard/pages/03_Live_CVE_Lookup.py

Lines changed: 204 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,17 @@
2323

2424
from src.clients.cisa_kev_client import fetch_kev_catalog
2525
from src.clients.epss_client import get_epss_score
26+
from src.clients.exa_client import is_exa_client_available, search_cve_related_content
2627
from src.clients.exploit_search_client import find_public_exploits, search_exploit_db, search_github
2728
from src.clients.nvd_client import fetch_single_cve_details
28-
from src.llm_analyzer import analyze_cve_async
29+
from src.llm_analyzer import analyze_article_content_async, analyze_cve_async
2930
from src.risk_analyzer import analyze_cve_risk
3031
from src.utils.config import get_db_file_name, get_llm_provider
3132
from src.utils.database_handler import (
3233
get_cve_details,
3334
store_cves,
3435
store_or_update_cve,
36+
store_threat_articles,
3537
update_cve_epss_data,
3638
update_cve_exploit_data,
3739
update_cve_kev_status,
@@ -409,6 +411,10 @@ def save_cve_to_database(cve_data):
409411
if use_github or use_exploitdb:
410412
tab_names.append("Search Exploits")
411413

414+
# Add threat intelligence tab if EXA AI is available
415+
if is_exa_client_available():
416+
tab_names.append("Threat Intelligence")
417+
412418
tabs = st.tabs(tab_names)
413419

414420
# Tab for manual analysis
@@ -539,6 +545,111 @@ def save_cve_to_database(cve_data):
539545
except Exception as e:
540546
st.error(f"Error searching for exploits: {str(e)}")
541547

548+
# Tab for threat intelligence if EXA AI is available
549+
if is_exa_client_available():
550+
# Determine which tab index to use based on whether exploit search tab exists
551+
threat_intel_tab_index = 3 if (use_github or use_exploitdb) else 2
552+
553+
with tabs[threat_intel_tab_index]:
554+
st.markdown("### 🌐 External Threat Intelligence Analysis")
555+
st.info(
556+
"Perform on-demand semantic search and analysis of external threat intelligence sources."
557+
)
558+
559+
if st.button(
560+
"🔍 Search & Analyze Threat Intelligence", type="primary", key="local_threat_intel"
561+
):
562+
analyzed_articles_local = []
563+
try:
564+
# Since Streamlit is sync, we run our async functions using asyncio.run()
565+
with st.spinner("Performing semantic search with EXA AI..."):
566+
# Step 1: Search for related content
567+
articles = asyncio.run(
568+
search_cve_related_content(cve_id, num_results=5)
569+
) # More results for local analysis
570+
571+
if articles:
572+
st.success(
573+
f"Found {len(articles)} potentially relevant articles. Analyzing with your configured LLM..."
574+
)
575+
576+
# Step 2: Analyze each article
577+
# Using st.progress for a better user experience
578+
progress_bar = st.progress(
579+
0, text="Analyzing content... (This may take a few minutes)"
580+
)
581+
for i, article in enumerate(articles):
582+
analysis_result = asyncio.run(
583+
analyze_article_content_async(article.get("text", ""))
584+
)
585+
if analysis_result:
586+
# Combine original article data with analysis results
587+
article.update(analysis_result)
588+
analyzed_articles_local.append(article)
589+
590+
# Update progress bar
591+
progress_bar.progress(
592+
(i + 1) / len(articles), text=f"Analyzing article {i+1}/{len(articles)}..."
593+
)
594+
595+
progress_bar.empty() # Remove progress bar when done
596+
st.info(f"Successfully analyzed {len(analyzed_articles_local)} articles.")
597+
598+
# Save the analyzed articles to the database
599+
if analyzed_articles_local:
600+
try:
601+
saved_count = store_threat_articles(
602+
analyzed_articles_local,
603+
source_query=f"CVE-related content for {cve_id}",
604+
cve_id_association=cve_id,
605+
)
606+
if saved_count > 0:
607+
st.success(
608+
f"💾 Saved {saved_count} analyzed articles to database for future reference."
609+
)
610+
else:
611+
st.info("📝 Articles were already in the database.")
612+
except Exception as save_error:
613+
st.warning(f"Could not save articles to database: {str(save_error)}")
614+
615+
else:
616+
st.warning("No relevant external articles found by EXA AI for this CVE.")
617+
618+
except Exception as e:
619+
st.error(f"An error occurred during threat intelligence gathering: {e}")
620+
621+
# Step 3: Display the results
622+
if analyzed_articles_local:
623+
st.markdown("### 📊 Threat Intelligence Analysis Results")
624+
625+
for article in analyzed_articles_local:
626+
with st.expander(f"📄 **{article.get('title', 'Untitled')}**"):
627+
st.markdown(f"**Source:** [{article.get('url')}]({article.get('url')})")
628+
629+
st.info(f"**AI Summary:**\n{article.get('summary', 'Not available.')}")
630+
631+
col1, col2 = st.columns(2)
632+
633+
with col1:
634+
st.write("**Mentioned Actors:**")
635+
st.json(article.get("mentioned_actors", []))
636+
st.write("**Mentioned Malware:**")
637+
st.json(article.get("mentioned_malware", []))
638+
639+
with col2:
640+
st.write("**Identified TTPs:**")
641+
st.json(article.get("identified_ttps", []))
642+
st.write("**Target Sectors:**")
643+
st.json(article.get("target_sectors", []))
644+
645+
st.write("**Extracted IOCs:**")
646+
# Use st.dataframe for better presentation of IOCs
647+
iocs_df = pd.DataFrame(article.get("extracted_iocs", []))
648+
if not iocs_df.empty:
649+
st.dataframe(iocs_df, use_container_width=True, hide_index=True)
650+
else:
651+
st.write("No IOCs extracted.")
652+
542653
else:
543654
# Not in local database, fetch from external sources
544655
st.info(f"{cve_id} not found in local database. Fetching live data...")
@@ -678,6 +789,98 @@ def save_cve_to_database(cve_data):
678789
st.markdown("## Analysis Results")
679790
display_cve_details(nvd_data, source="Live Data")
680791

792+
# --- On-Demand External Threat Intelligence Workflow ---
793+
if is_exa_client_available():
794+
st.markdown("---")
795+
st.subheader("🌐 Live External Threat Intelligence Analysis")
796+
797+
analyzed_articles_live = []
798+
try:
799+
# Since Streamlit is sync, we run our async functions using asyncio.run()
800+
with st.spinner("Performing live semantic search with EXA AI..."):
801+
# Step 1: Search for related content
802+
articles = asyncio.run(
803+
search_cve_related_content(cve_id, num_results=3)
804+
) # Limit to 3 for faster live analysis
805+
806+
if articles:
807+
st.success(
808+
f"Found {len(articles)} potentially relevant articles. Analyzing with your configured LLM..."
809+
)
810+
811+
# Step 2: Analyze each article
812+
# Using st.progress for a better user experience
813+
progress_bar = st.progress(0, text="Analyzing content... (This may take a moment)")
814+
for i, article in enumerate(articles):
815+
analysis_result = asyncio.run(
816+
analyze_article_content_async(article.get("text", ""))
817+
)
818+
if analysis_result:
819+
# Combine original article data with analysis results
820+
article.update(analysis_result)
821+
analyzed_articles_live.append(article)
822+
823+
# Update progress bar
824+
progress_bar.progress(
825+
(i + 1) / len(articles), text=f"Analyzing article {i+1}/{len(articles)}..."
826+
)
827+
828+
progress_bar.empty() # Remove progress bar when done
829+
st.info(f"Successfully analyzed {len(analyzed_articles_live)} articles.")
830+
831+
# Save the analyzed articles to the database
832+
if analyzed_articles_live:
833+
try:
834+
saved_count = store_threat_articles(
835+
analyzed_articles_live,
836+
source_query=f"CVE-related content for {cve_id}",
837+
cve_id_association=cve_id,
838+
)
839+
if saved_count > 0:
840+
st.success(
841+
f"💾 Saved {saved_count} analyzed articles to database for future reference."
842+
)
843+
else:
844+
st.info("📝 Articles were already in the database.")
845+
except Exception as save_error:
846+
st.warning(f"Could not save articles to database: {str(save_error)}")
847+
848+
else:
849+
st.warning("No relevant external articles found by EXA AI for this CVE.")
850+
851+
except Exception as e:
852+
st.error(f"An error occurred during live threat intelligence gathering: {e}")
853+
854+
# Step 3: Display the results
855+
if analyzed_articles_live:
856+
for article in analyzed_articles_live:
857+
with st.expander(f"📄 **{article.get('title', 'Untitled')}**"):
858+
st.markdown(f"**Source:** [{article.get('url')}]({article.get('url')})")
859+
860+
st.info(f"**AI Summary:**\n{article.get('summary', 'Not available.')}")
861+
862+
col1, col2 = st.columns(2)
863+
864+
with col1:
865+
st.write("**Mentioned Actors:**")
866+
st.json(article.get("mentioned_actors", []))
867+
st.write("**Mentioned Malware:**")
868+
st.json(article.get("mentioned_malware", []))
869+
870+
with col2:
871+
st.write("**Identified TTPs:**")
872+
st.json(article.get("identified_ttps", []))
873+
st.write("**Target Sectors:**")
874+
st.json(article.get("target_sectors", []))
875+
876+
st.write("**Extracted IOCs:**")
877+
# Use st.dataframe for better presentation of IOCs
878+
iocs_df = pd.DataFrame(article.get("extracted_iocs", []))
879+
if not iocs_df.empty:
880+
st.dataframe(iocs_df, use_container_width=True, hide_index=True)
881+
else:
882+
st.write("No IOCs extracted.")
883+
681884
st.info("💾 You can now save this CVE to your database")
682885
else:
683886
st.error(f"Could not find {cve_id} in NVD database or an error occurred.")

src/utils/database_handler.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -850,7 +850,8 @@ def get_filtered_cves(
850850
params.append(1 if has_public_exploit else 0)
851851

852852
if keyword:
853-
query += " AND description LIKE ?"
853+
query += " AND (description LIKE ? OR cve_id LIKE ?)"
854+
params.append(f"%{keyword}%")
854855
params.append(f"%{keyword}%")
855856

856857
# Microsoft-specific filters

0 commit comments

Comments
 (0)