|
23 | 23 |
|
24 | 24 | from src.clients.cisa_kev_client import fetch_kev_catalog |
25 | 25 | from src.clients.epss_client import get_epss_score |
| 26 | +from src.clients.exa_client import is_exa_client_available, search_cve_related_content |
26 | 27 | from src.clients.exploit_search_client import find_public_exploits, search_exploit_db, search_github |
27 | 28 | 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 |
29 | 30 | from src.risk_analyzer import analyze_cve_risk |
30 | 31 | from src.utils.config import get_db_file_name, get_llm_provider |
31 | 32 | from src.utils.database_handler import ( |
32 | 33 | get_cve_details, |
33 | 34 | store_cves, |
34 | 35 | store_or_update_cve, |
| 36 | + store_threat_articles, |
35 | 37 | update_cve_epss_data, |
36 | 38 | update_cve_exploit_data, |
37 | 39 | update_cve_kev_status, |
@@ -409,6 +411,10 @@ def save_cve_to_database(cve_data): |
409 | 411 | if use_github or use_exploitdb: |
410 | 412 | tab_names.append("Search Exploits") |
411 | 413 |
|
| 414 | + # Add threat intelligence tab if EXA AI is available |
| 415 | + if is_exa_client_available(): |
| 416 | + tab_names.append("Threat Intelligence") |
| 417 | + |
412 | 418 | tabs = st.tabs(tab_names) |
413 | 419 |
|
414 | 420 | # Tab for manual analysis |
@@ -539,6 +545,111 @@ def save_cve_to_database(cve_data): |
539 | 545 | except Exception as e: |
540 | 546 | st.error(f"Error searching for exploits: {str(e)}") |
541 | 547 |
|
| 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 | + |
542 | 653 | else: |
543 | 654 | # Not in local database, fetch from external sources |
544 | 655 | st.info(f"{cve_id} not found in local database. Fetching live data...") |
@@ -678,6 +789,98 @@ def save_cve_to_database(cve_data): |
678 | 789 | st.markdown("## Analysis Results") |
679 | 790 | display_cve_details(nvd_data, source="Live Data") |
680 | 791 |
|
| 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 | + |
681 | 884 | st.info("💾 You can now save this CVE to your database") |
682 | 885 | else: |
683 | 886 | st.error(f"Could not find {cve_id} in NVD database or an error occurred.") |
|
0 commit comments