souleyez 3.0.0__py3-none-any.whl → 3.0.9__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of souleyez might be problematic. Click here for more details.
- souleyez/__init__.py +1 -1
- souleyez/ai/__init__.py +7 -7
- souleyez/ai/action_mapper.py +3 -2
- souleyez/ai/chain_advisor.py +2 -1
- souleyez/ai/claude_provider.py +2 -2
- souleyez/ai/context_builder.py +4 -2
- souleyez/ai/executor.py +9 -6
- souleyez/ai/feedback_handler.py +4 -2
- souleyez/ai/llm_provider.py +2 -2
- souleyez/ai/ollama_provider.py +2 -2
- souleyez/ai/ollama_service.py +10 -26
- souleyez/ai/path_scorer.py +2 -1
- souleyez/ai/recommender.py +6 -4
- souleyez/ai/report_context.py +2 -2
- souleyez/ai/report_service.py +5 -5
- souleyez/ai/result_parser.py +3 -2
- souleyez/ai/safety.py +5 -2
- souleyez/auth/__init__.py +6 -6
- souleyez/auth/audit.py +2 -2
- souleyez/auth/engagement_access.py +5 -7
- souleyez/auth/permissions.py +1 -1
- souleyez/auth/session_manager.py +5 -5
- souleyez/auth/user_manager.py +4 -5
- souleyez/commands/audit.py +6 -5
- souleyez/commands/auth.py +6 -5
- souleyez/commands/deliverables.py +2 -3
- souleyez/commands/engagement.py +3 -3
- souleyez/commands/license.py +3 -2
- souleyez/commands/screenshots.py +5 -4
- souleyez/commands/user.py +10 -8
- souleyez/config.py +4 -2
- souleyez/core/credential_tester.py +4 -2
- souleyez/core/cve_mappings.py +2 -1
- souleyez/core/cve_matcher.py +2 -1
- souleyez/core/msf_auto_mapper.py +2 -0
- souleyez/core/msf_chain_engine.py +3 -1
- souleyez/core/msf_database.py +7 -13
- souleyez/core/msf_integration.py +2 -2
- souleyez/core/msf_rpc_client.py +3 -2
- souleyez/core/msf_rpc_manager.py +4 -4
- souleyez/core/msf_sync_manager.py +7 -7
- souleyez/core/network_utils.py +1 -1
- souleyez/core/parser_handler.py +2 -1
- souleyez/core/pending_chains.py +4 -3
- souleyez/core/templates.py +5 -2
- souleyez/core/tool_chaining.py +101 -70
- souleyez/core/version_utils.py +1 -0
- souleyez/core/vuln_correlation.py +3 -2
- souleyez/core/web_utils.py +2 -1
- souleyez/detection/__init__.py +1 -1
- souleyez/detection/attack_signatures.py +1 -1
- souleyez/detection/mitre_mappings.py +1 -2
- souleyez/detection/validator.py +5 -4
- souleyez/devtools.py +4 -2
- souleyez/docs/README.md +2 -2
- souleyez/engine/background.py +168 -7
- souleyez/engine/base.py +2 -1
- souleyez/engine/loader.py +4 -2
- souleyez/engine/log_sanitizer.py +1 -0
- souleyez/engine/manager.py +3 -1
- souleyez/engine/result_handler.py +50 -67
- souleyez/engine/worker_manager.py +6 -4
- souleyez/export/evidence_bundle.py +1 -0
- souleyez/handlers/base.py +1 -0
- souleyez/handlers/bash_handler.py +1 -0
- souleyez/handlers/bloodhound_handler.py +1 -0
- souleyez/handlers/certipy_handler.py +1 -0
- souleyez/handlers/crackmapexec_handler.py +2 -20
- souleyez/handlers/dnsrecon_handler.py +2 -1
- souleyez/handlers/enum4linux_handler.py +65 -37
- souleyez/handlers/evil_winrm_handler.py +1 -0
- souleyez/handlers/ffuf_handler.py +3 -1
- souleyez/handlers/gobuster_handler.py +7 -6
- souleyez/handlers/gpp_extract_handler.py +1 -0
- souleyez/handlers/hashcat_handler.py +1 -0
- souleyez/handlers/hydra_handler.py +5 -2
- souleyez/handlers/impacket_getuserspns_handler.py +1 -0
- souleyez/handlers/impacket_psexec_handler.py +1 -0
- souleyez/handlers/impacket_secretsdump_handler.py +1 -0
- souleyez/handlers/john_handler.py +1 -0
- souleyez/handlers/katana_handler.py +39 -2
- souleyez/handlers/kerbrute_handler.py +1 -0
- souleyez/handlers/ldapsearch_handler.py +90 -17
- souleyez/handlers/lfi_extract_handler.py +1 -0
- souleyez/handlers/msf_auxiliary_handler.py +1 -0
- souleyez/handlers/msf_exploit_handler.py +1 -0
- souleyez/handlers/nikto_handler.py +2 -1
- souleyez/handlers/nmap_handler.py +2 -1
- souleyez/handlers/nuclei_handler.py +2 -1
- souleyez/handlers/nxc_handler.py +3 -18
- souleyez/handlers/rdp_sec_check_handler.py +1 -0
- souleyez/handlers/registry.py +1 -0
- souleyez/handlers/responder_handler.py +1 -0
- souleyez/handlers/service_explorer_handler.py +2 -1
- souleyez/handlers/smbclient_handler.py +1 -0
- souleyez/handlers/smbmap_handler.py +3 -2
- souleyez/handlers/sqlmap_handler.py +6 -4
- souleyez/handlers/theharvester_handler.py +2 -1
- souleyez/handlers/web_login_test_handler.py +1 -0
- souleyez/handlers/whois_handler.py +3 -2
- souleyez/handlers/wpscan_handler.py +2 -1
- souleyez/history.py +4 -3
- souleyez/importers/msf_importer.py +5 -3
- souleyez/importers/smart_importer.py +6 -4
- souleyez/integrations/siem/__init__.py +6 -6
- souleyez/integrations/siem/base.py +1 -1
- souleyez/integrations/siem/elastic.py +3 -3
- souleyez/integrations/siem/factory.py +1 -2
- souleyez/integrations/siem/googlesecops.py +4 -4
- souleyez/integrations/siem/rule_mappings/wazuh_rules.py +1 -1
- souleyez/integrations/siem/sentinel.py +3 -3
- souleyez/integrations/siem/splunk.py +3 -3
- souleyez/integrations/siem/wazuh.py +4 -4
- souleyez/integrations/wazuh/__init__.py +1 -1
- souleyez/integrations/wazuh/client.py +3 -2
- souleyez/integrations/wazuh/config.py +3 -2
- souleyez/integrations/wazuh/host_mapper.py +3 -1
- souleyez/integrations/wazuh/sync.py +4 -1
- souleyez/intelligence/__init__.py +1 -1
- souleyez/intelligence/correlation_analyzer.py +6 -5
- souleyez/intelligence/exploit_knowledge.py +4 -4
- souleyez/intelligence/exploit_suggestions.py +4 -3
- souleyez/intelligence/gap_analyzer.py +5 -3
- souleyez/intelligence/gap_detector.py +2 -0
- souleyez/intelligence/sensitive_tables.py +1 -1
- souleyez/intelligence/service_parser.py +1 -0
- souleyez/intelligence/surface_analyzer.py +9 -9
- souleyez/intelligence/target_parser.py +1 -0
- souleyez/licensing/__init__.py +3 -3
- souleyez/main.py +25 -18
- souleyez/migrations/fix_job_counter.py +2 -1
- souleyez/parsers/bloodhound_parser.py +1 -0
- souleyez/parsers/crackmapexec_parser.py +2 -1
- souleyez/parsers/dalfox_parser.py +3 -2
- souleyez/parsers/dnsrecon_parser.py +2 -1
- souleyez/parsers/enum4linux_parser.py +2 -1
- souleyez/parsers/ffuf_parser.py +2 -1
- souleyez/parsers/gobuster_parser.py +2 -1
- souleyez/parsers/hashcat_parser.py +3 -2
- souleyez/parsers/http_fingerprint_parser.py +2 -1
- souleyez/parsers/hydra_parser.py +2 -1
- souleyez/parsers/impacket_parser.py +2 -1
- souleyez/parsers/john_parser.py +4 -3
- souleyez/parsers/katana_parser.py +134 -2
- souleyez/parsers/msf_parser.py +2 -1
- souleyez/parsers/nikto_parser.py +2 -1
- souleyez/parsers/nmap_parser.py +14 -3
- souleyez/parsers/nuclei_parser.py +3 -2
- souleyez/parsers/responder_parser.py +1 -0
- souleyez/parsers/searchsploit_parser.py +3 -2
- souleyez/parsers/service_explorer_parser.py +1 -0
- souleyez/parsers/smbmap_parser.py +2 -1
- souleyez/parsers/sqlmap_parser.py +36 -2
- souleyez/parsers/theharvester_parser.py +2 -1
- souleyez/parsers/whois_parser.py +2 -1
- souleyez/parsers/wpscan_parser.py +3 -2
- souleyez/plugins/afp.py +3 -1
- souleyez/plugins/afp_brute.py +3 -1
- souleyez/plugins/ard.py +3 -1
- souleyez/plugins/bloodhound.py +3 -2
- souleyez/plugins/certipy.py +1 -0
- souleyez/plugins/crackmapexec.py +11 -7
- souleyez/plugins/dalfox.py +5 -2
- souleyez/plugins/dns_hijack.py +3 -1
- souleyez/plugins/dnsrecon.py +3 -1
- souleyez/plugins/enum4linux.py +3 -1
- souleyez/plugins/evil_winrm.py +1 -0
- souleyez/plugins/ffuf.py +3 -1
- souleyez/plugins/firmware_extract.py +3 -2
- souleyez/plugins/gobuster.py +6 -3
- souleyez/plugins/gpp_extract.py +1 -0
- souleyez/plugins/hashcat.py +2 -1
- souleyez/plugins/http_fingerprint.py +57 -7
- souleyez/plugins/hydra.py +5 -3
- souleyez/plugins/impacket_common.py +40 -0
- souleyez/plugins/impacket_getnpusers.py +19 -2
- souleyez/plugins/impacket_getuserspns.py +158 -0
- souleyez/plugins/impacket_psexec.py +19 -2
- souleyez/plugins/impacket_secretsdump.py +19 -2
- souleyez/plugins/impacket_smbclient.py +19 -2
- souleyez/plugins/john.py +2 -1
- souleyez/plugins/katana.py +48 -6
- souleyez/plugins/kerbrute.py +1 -0
- souleyez/plugins/lfi_extract.py +1 -0
- souleyez/plugins/macos_ssh.py +3 -1
- souleyez/plugins/mdns.py +3 -1
- souleyez/plugins/msf_auxiliary.py +3 -2
- souleyez/plugins/msf_exploit.py +6 -5
- souleyez/plugins/nikto.py +5 -2
- souleyez/plugins/nmap.py +6 -4
- souleyez/plugins/nuclei.py +3 -1
- souleyez/plugins/nxc.py +1 -0
- souleyez/plugins/plugin_base.py +3 -2
- souleyez/plugins/plugin_template.py +3 -2
- souleyez/plugins/rdp_sec_check.py +1 -0
- souleyez/plugins/responder.py +2 -1
- souleyez/plugins/router_http_brute.py +3 -1
- souleyez/plugins/router_ssh_brute.py +3 -1
- souleyez/plugins/router_telnet_brute.py +3 -1
- souleyez/plugins/routersploit.py +5 -3
- souleyez/plugins/routersploit_exploit.py +5 -3
- souleyez/plugins/searchsploit.py +1 -0
- souleyez/plugins/service_explorer.py +2 -1
- souleyez/plugins/smbmap.py +3 -1
- souleyez/plugins/smbpasswd.py +1 -0
- souleyez/plugins/sqlmap.py +3 -1
- souleyez/plugins/theharvester.py +3 -1
- souleyez/plugins/tr069.py +3 -1
- souleyez/plugins/upnp.py +3 -1
- souleyez/plugins/upnp_abuse.py +4 -2
- souleyez/plugins/vnc_access.py +4 -2
- souleyez/plugins/vnc_brute.py +3 -1
- souleyez/plugins/web_login_test.py +1 -0
- souleyez/plugins/whois.py +3 -1
- souleyez/plugins/wpscan.py +3 -1
- souleyez/reporting/attack_chain.py +2 -1
- souleyez/reporting/charts.py +1 -0
- souleyez/reporting/compliance_mappings.py +1 -0
- souleyez/reporting/detection_report.py +10 -10
- souleyez/reporting/formatters.py +7 -12
- souleyez/reporting/generator.py +34 -46
- souleyez/reporting/metrics.py +2 -1
- souleyez/scanner.py +6 -3
- souleyez/security/__init__.py +7 -5
- souleyez/security/scope_validator.py +5 -4
- souleyez/security.py +5 -2
- souleyez/storage/credentials.py +14 -19
- souleyez/storage/crypto.py +7 -4
- souleyez/storage/database.py +6 -6
- souleyez/storage/db.py +8 -8
- souleyez/storage/deliverable_evidence.py +2 -1
- souleyez/storage/deliverable_exporter.py +3 -2
- souleyez/storage/deliverable_templates.py +2 -1
- souleyez/storage/deliverables.py +2 -1
- souleyez/storage/engagements.py +6 -4
- souleyez/storage/evidence.py +5 -4
- souleyez/storage/execution_log.py +4 -2
- souleyez/storage/exploit_attempts.py +3 -2
- souleyez/storage/exploits.py +3 -1
- souleyez/storage/findings.py +3 -1
- souleyez/storage/hosts.py +5 -2
- souleyez/storage/migrate_to_engagements.py +14 -24
- souleyez/storage/migrations/_001_add_credential_enhancements.py +12 -21
- souleyez/storage/migrations/_003_add_execution_log.py +8 -13
- souleyez/storage/migrations/_005_screenshots.py +2 -4
- souleyez/storage/migrations/_006_deliverables.py +2 -4
- souleyez/storage/migrations/_007_deliverable_templates.py +4 -8
- souleyez/storage/migrations/_008_add_nuclei_table.py +2 -4
- souleyez/storage/migrations/_010_evidence_linking.py +6 -12
- souleyez/storage/migrations/_012_team_collaboration.py +12 -24
- souleyez/storage/migrations/_013_add_host_tags.py +2 -4
- souleyez/storage/migrations/_014_exploit_attempts.py +10 -20
- souleyez/storage/migrations/_015_add_mac_os_fields.py +4 -8
- souleyez/storage/migrations/_016_add_domain_field.py +2 -4
- souleyez/storage/migrations/_017_msf_sessions.py +8 -16
- souleyez/storage/migrations/_018_add_osint_target.py +4 -8
- souleyez/storage/migrations/_019_add_engagement_type.py +4 -8
- souleyez/storage/migrations/_020_add_rbac.py +9 -17
- souleyez/storage/migrations/_021_wazuh_integration.py +4 -8
- souleyez/storage/migrations/_023_fix_detection_results_fk.py +2 -4
- souleyez/storage/migrations/_024_wazuh_vulnerabilities.py +4 -8
- souleyez/storage/migrations/_026_add_engagement_scope.py +4 -8
- souleyez/storage/migrations/_027_multi_siem_persistence.py +8 -16
- souleyez/storage/migrations/__init__.py +1 -4
- souleyez/storage/migrations/migration_manager.py +6 -9
- souleyez/storage/msf_sessions.py +1 -1
- souleyez/storage/osint.py +3 -1
- souleyez/storage/recommendation_engine.py +3 -2
- souleyez/storage/screenshots.py +2 -1
- souleyez/storage/smb_shares.py +3 -1
- souleyez/storage/sqlmap_data.py +6 -4
- souleyez/storage/team_collaboration.py +3 -2
- souleyez/storage/timeline_tracker.py +2 -1
- souleyez/storage/wazuh_vulns.py +3 -1
- souleyez/storage/web_paths.py +3 -1
- souleyez/testing/credential_tester.py +2 -0
- souleyez/ui/__init__.py +2 -1
- souleyez/ui/ai_quotes.py +1 -1
- souleyez/ui/attack_surface.py +50 -28
- souleyez/ui/chain_rules_view.py +6 -3
- souleyez/ui/correlation_view.py +3 -2
- souleyez/ui/dashboard.py +85 -139
- souleyez/ui/deliverables_view.py +1 -1
- souleyez/ui/design_system.py +5 -3
- souleyez/ui/errors.py +3 -1
- souleyez/ui/evidence_linking_view.py +2 -1
- souleyez/ui/evidence_vault.py +11 -6
- souleyez/ui/exploit_suggestions_view.py +11 -7
- souleyez/ui/export_view.py +3 -1
- souleyez/ui/gap_analysis_view.py +6 -3
- souleyez/ui/help_system.py +4 -1
- souleyez/ui/intelligence_view.py +7 -3
- souleyez/ui/interactive.py +1280 -558
- souleyez/ui/interactive_selector.py +3 -2
- souleyez/ui/log_formatter.py +1 -0
- souleyez/ui/menu_components.py +3 -1
- souleyez/ui/msf_auxiliary_menu.py +4 -1
- souleyez/ui/pending_chains_view.py +15 -12
- souleyez/ui/progress_indicators.py +5 -2
- souleyez/ui/recommendations_view.py +4 -2
- souleyez/ui/rule_builder.py +4 -1
- souleyez/ui/setup_wizard.py +10 -8
- souleyez/ui/shortcuts.py +1 -1
- souleyez/ui/splunk_gap_analysis_view.py +7 -4
- souleyez/ui/splunk_vulns_view.py +4 -1
- souleyez/ui/team_dashboard.py +7 -5
- souleyez/ui/template_selector.py +2 -1
- souleyez/ui/terminal.py +3 -2
- souleyez/ui/timeline_view.py +2 -1
- souleyez/ui/tool_setup.py +92 -31
- souleyez/ui/tutorial.py +7 -4
- souleyez/ui/tutorial_state.py +3 -2
- souleyez/ui/wazuh_vulns_view.py +5 -2
- souleyez/ui/wordlist_browser.py +4 -3
- souleyez/ui.py +13 -7
- souleyez/utils/tool_checker.py +95 -17
- souleyez/utils.py +4 -4
- souleyez/wordlists.py +1 -0
- {souleyez-3.0.0.dist-info → souleyez-3.0.9.dist-info}/METADATA +1 -1
- souleyez-3.0.9.dist-info/RECORD +445 -0
- souleyez-3.0.0.dist-info/RECORD +0 -443
- {souleyez-3.0.0.dist-info → souleyez-3.0.9.dist-info}/WHEEL +0 -0
- {souleyez-3.0.0.dist-info → souleyez-3.0.9.dist-info}/entry_points.txt +0 -0
- {souleyez-3.0.0.dist-info → souleyez-3.0.9.dist-info}/licenses/LICENSE +0 -0
- {souleyez-3.0.0.dist-info → souleyez-3.0.9.dist-info}/top_level.txt +0 -0
souleyez/reporting/generator.py
CHANGED
|
@@ -3,11 +3,12 @@
|
|
|
3
3
|
Professional penetration test report generator.
|
|
4
4
|
Creates comprehensive, client-ready reports in multiple formats.
|
|
5
5
|
"""
|
|
6
|
-
|
|
6
|
+
|
|
7
7
|
import logging
|
|
8
|
+
import os
|
|
8
9
|
from datetime import datetime
|
|
9
|
-
from typing import Dict, List, Optional
|
|
10
10
|
from pathlib import Path
|
|
11
|
+
from typing import Dict, List, Optional
|
|
11
12
|
|
|
12
13
|
logger = logging.getLogger(__name__)
|
|
13
14
|
|
|
@@ -16,11 +17,11 @@ class ReportGenerator:
|
|
|
16
17
|
"""Generates professional pentest reports from engagement data."""
|
|
17
18
|
|
|
18
19
|
def __init__(self):
|
|
20
|
+
from souleyez.intelligence.surface_analyzer import AttackSurfaceAnalyzer
|
|
21
|
+
from souleyez.storage.credentials import CredentialsManager
|
|
19
22
|
from souleyez.storage.engagements import EngagementManager
|
|
20
23
|
from souleyez.storage.evidence import EvidenceManager
|
|
21
|
-
from souleyez.intelligence.surface_analyzer import AttackSurfaceAnalyzer
|
|
22
24
|
from souleyez.storage.findings import FindingsManager
|
|
23
|
-
from souleyez.storage.credentials import CredentialsManager
|
|
24
25
|
|
|
25
26
|
self.em = EngagementManager()
|
|
26
27
|
self.evm = EvidenceManager()
|
|
@@ -319,12 +320,13 @@ class ReportGenerator:
|
|
|
319
320
|
|
|
320
321
|
def _generate_html(self, data: Dict) -> str:
|
|
321
322
|
"""Generate HTML report with enhanced visualizations."""
|
|
322
|
-
|
|
323
|
-
|
|
323
|
+
import markdown
|
|
324
|
+
|
|
325
|
+
from souleyez.reporting.attack_chain import AttackChainAnalyzer
|
|
324
326
|
from souleyez.reporting.charts import ChartGenerator
|
|
325
327
|
from souleyez.reporting.compliance_mappings import ComplianceMappings
|
|
326
|
-
from souleyez.reporting.
|
|
327
|
-
import
|
|
328
|
+
from souleyez.reporting.formatters import HTMLFormatter, MarkdownFormatter
|
|
329
|
+
from souleyez.reporting.metrics import MetricsCalculator
|
|
328
330
|
|
|
329
331
|
formatter = HTMLFormatter()
|
|
330
332
|
md_formatter = MarkdownFormatter()
|
|
@@ -554,7 +556,7 @@ class ReportGenerator:
|
|
|
554
556
|
|
|
555
557
|
# Try WeasyPrint first (preferred - pure Python, better CSS support)
|
|
556
558
|
try:
|
|
557
|
-
from weasyprint import
|
|
559
|
+
from weasyprint import CSS, HTML
|
|
558
560
|
from weasyprint.text.fonts import FontConfiguration
|
|
559
561
|
|
|
560
562
|
font_config = FontConfiguration()
|
|
@@ -869,11 +871,12 @@ class ReportGenerator:
|
|
|
869
871
|
Returns:
|
|
870
872
|
Path to generated report file
|
|
871
873
|
"""
|
|
874
|
+
import markdown
|
|
875
|
+
|
|
876
|
+
from souleyez.integrations.wazuh.config import WazuhConfig
|
|
877
|
+
from souleyez.reporting.charts import ChartGenerator
|
|
872
878
|
from souleyez.reporting.detection_report import DetectionReportGatherer
|
|
873
879
|
from souleyez.reporting.formatters import HTMLFormatter, MarkdownFormatter
|
|
874
|
-
from souleyez.reporting.charts import ChartGenerator
|
|
875
|
-
from souleyez.integrations.wazuh.config import WazuhConfig
|
|
876
|
-
import markdown
|
|
877
880
|
|
|
878
881
|
# Check if Wazuh is configured
|
|
879
882
|
config = WazuhConfig.get_config(engagement_id)
|
|
@@ -963,22 +966,21 @@ class ReportGenerator:
|
|
|
963
966
|
sections.append(formatter.per_host_detection_section(data))
|
|
964
967
|
|
|
965
968
|
# Footer
|
|
966
|
-
sections.append(
|
|
967
|
-
f"""
|
|
969
|
+
sections.append(f"""
|
|
968
970
|
---
|
|
969
971
|
|
|
970
972
|
*Detection Coverage Report generated by SoulEyez*
|
|
971
973
|
*{data.generated_at.strftime('%B %d, %Y at %H:%M:%S')}*
|
|
972
|
-
"""
|
|
973
|
-
)
|
|
974
|
+
""")
|
|
974
975
|
|
|
975
976
|
return "\n\n".join(sections)
|
|
976
977
|
|
|
977
978
|
def _generate_detection_html(self, data, charts: Dict) -> str:
|
|
978
979
|
"""Generate detection report in HTML format with heatmap."""
|
|
979
|
-
from souleyez.reporting.formatters import HTMLFormatter
|
|
980
980
|
import markdown
|
|
981
981
|
|
|
982
|
+
from souleyez.reporting.formatters import HTMLFormatter
|
|
983
|
+
|
|
982
984
|
# Helper to convert markdown with table support
|
|
983
985
|
def md_to_html(md_text: str) -> str:
|
|
984
986
|
return markdown.markdown(md_text, extensions=["tables", "fenced_code"])
|
|
@@ -1005,8 +1007,7 @@ class ReportGenerator:
|
|
|
1005
1007
|
|
|
1006
1008
|
# Stats cards
|
|
1007
1009
|
summary = data.summary
|
|
1008
|
-
sections.append(
|
|
1009
|
-
f"""
|
|
1010
|
+
sections.append(f"""
|
|
1010
1011
|
<div class="detection-stat-grid">
|
|
1011
1012
|
<div class="detection-stat-card coverage">
|
|
1012
1013
|
<div class="detection-stat-value">{summary.coverage_percent}%</div>
|
|
@@ -1025,8 +1026,7 @@ class ReportGenerator:
|
|
|
1025
1026
|
<div class="detection-stat-label">Not Detected</div>
|
|
1026
1027
|
</div>
|
|
1027
1028
|
</div>
|
|
1028
|
-
"""
|
|
1029
|
-
)
|
|
1029
|
+
""")
|
|
1030
1030
|
|
|
1031
1031
|
# Coverage overview
|
|
1032
1032
|
overview_md = formatter.detection_coverage_overview(data)
|
|
@@ -1034,28 +1034,24 @@ class ReportGenerator:
|
|
|
1034
1034
|
|
|
1035
1035
|
# Charts
|
|
1036
1036
|
if charts.get("detection_coverage"):
|
|
1037
|
-
sections.append(
|
|
1038
|
-
f"""
|
|
1037
|
+
sections.append(f"""
|
|
1039
1038
|
<div class="chart-container" style="max-width: 400px; margin: 20px auto;">
|
|
1040
1039
|
<canvas id="detectionCoverageChart"></canvas>
|
|
1041
1040
|
</div>
|
|
1042
1041
|
<script>
|
|
1043
1042
|
new Chart(document.getElementById('detectionCoverageChart'), {charts['detection_coverage']});
|
|
1044
1043
|
</script>
|
|
1045
|
-
"""
|
|
1046
|
-
)
|
|
1044
|
+
""")
|
|
1047
1045
|
|
|
1048
1046
|
if charts.get("detection_by_tactic"):
|
|
1049
|
-
sections.append(
|
|
1050
|
-
f"""
|
|
1047
|
+
sections.append(f"""
|
|
1051
1048
|
<div class="chart-container" style="max-width: 800px; margin: 20px auto;">
|
|
1052
1049
|
<canvas id="detectionTacticChart"></canvas>
|
|
1053
1050
|
</div>
|
|
1054
1051
|
<script>
|
|
1055
1052
|
new Chart(document.getElementById('detectionTacticChart'), {charts['detection_by_tactic']});
|
|
1056
1053
|
</script>
|
|
1057
|
-
"""
|
|
1058
|
-
)
|
|
1054
|
+
""")
|
|
1059
1055
|
|
|
1060
1056
|
# MITRE ATT&CK Heatmap (HTML version)
|
|
1061
1057
|
sections.append(formatter.mitre_heatmap_html(data))
|
|
@@ -1078,14 +1074,12 @@ new Chart(document.getElementById('detectionTacticChart'), {charts['detection_by
|
|
|
1078
1074
|
|
|
1079
1075
|
# Detection gaps (with warning styling)
|
|
1080
1076
|
if data.gaps:
|
|
1081
|
-
sections.append(
|
|
1082
|
-
"""
|
|
1077
|
+
sections.append("""
|
|
1083
1078
|
<div class="gap-warning">
|
|
1084
1079
|
<h4>Detection Gaps Identified</h4>
|
|
1085
1080
|
<p>The following attacks were NOT detected by the SIEM. These represent potential blindspots that should be addressed.</p>
|
|
1086
1081
|
</div>
|
|
1087
|
-
"""
|
|
1088
|
-
)
|
|
1082
|
+
""")
|
|
1089
1083
|
gaps_md = formatter.detection_gaps_section(data)
|
|
1090
1084
|
sections.append(md_to_html(gaps_md))
|
|
1091
1085
|
|
|
@@ -1102,22 +1096,18 @@ new Chart(document.getElementById('detectionTacticChart'), {charts['detection_by
|
|
|
1102
1096
|
sections.append(md_to_html(host_md))
|
|
1103
1097
|
|
|
1104
1098
|
# Footer
|
|
1105
|
-
sections.append(
|
|
1106
|
-
f"""
|
|
1099
|
+
sections.append(f"""
|
|
1107
1100
|
<hr>
|
|
1108
1101
|
<p style="text-align: center; color: #6c757d;">
|
|
1109
1102
|
<em>Detection Coverage Report generated by SoulEyez</em><br>
|
|
1110
1103
|
{data.generated_at.strftime('%B %d, %Y at %H:%M:%S')}
|
|
1111
1104
|
</p>
|
|
1112
|
-
"""
|
|
1113
|
-
)
|
|
1105
|
+
""")
|
|
1114
1106
|
|
|
1115
1107
|
# HTML footer with Chart.js
|
|
1116
|
-
sections.append(
|
|
1117
|
-
"""
|
|
1108
|
+
sections.append("""
|
|
1118
1109
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js"></script>
|
|
1119
|
-
"""
|
|
1120
|
-
)
|
|
1110
|
+
""")
|
|
1121
1111
|
sections.append(formatter.html_footer())
|
|
1122
1112
|
|
|
1123
1113
|
return "\n".join(sections)
|
|
@@ -1135,7 +1125,7 @@ new Chart(document.getElementById('detectionTacticChart'), {charts['detection_by
|
|
|
1135
1125
|
|
|
1136
1126
|
# Try WeasyPrint first (better CSS support)
|
|
1137
1127
|
try:
|
|
1138
|
-
from weasyprint import
|
|
1128
|
+
from weasyprint import CSS, HTML
|
|
1139
1129
|
|
|
1140
1130
|
# Inline all external resources for PDF
|
|
1141
1131
|
html_content = html_content.replace(
|
|
@@ -1143,8 +1133,7 @@ new Chart(document.getElementById('detectionTacticChart'), {charts['detection_by
|
|
|
1143
1133
|
)
|
|
1144
1134
|
|
|
1145
1135
|
html = HTML(string=html_content)
|
|
1146
|
-
css = CSS(
|
|
1147
|
-
string="""
|
|
1136
|
+
css = CSS(string="""
|
|
1148
1137
|
@page {
|
|
1149
1138
|
size: letter;
|
|
1150
1139
|
margin: 20mm 15mm;
|
|
@@ -1161,8 +1150,7 @@ new Chart(document.getElementById('detectionTacticChart'), {charts['detection_by
|
|
|
1161
1150
|
.mitre-heatmap {
|
|
1162
1151
|
page-break-inside: avoid;
|
|
1163
1152
|
}
|
|
1164
|
-
"""
|
|
1165
|
-
)
|
|
1153
|
+
""")
|
|
1166
1154
|
html.write_pdf(output_path, stylesheets=[css])
|
|
1167
1155
|
logger.info(f"PDF generated with WeasyPrint: {output_path}")
|
|
1168
1156
|
return output_path
|
souleyez/reporting/metrics.py
CHANGED
souleyez/scanner.py
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
import subprocess
|
|
3
|
-
import defusedxml.ElementTree as ET
|
|
4
3
|
from pathlib import Path
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
|
|
5
|
+
import defusedxml.ElementTree as ET
|
|
6
|
+
|
|
7
7
|
from souleyez.log_config import get_logger
|
|
8
|
+
from souleyez.security.validation import ValidationError, validate_nmap_args
|
|
9
|
+
|
|
10
|
+
from .utils import SCANS_DIR, ensure_dirs, join_cmd, nmap_installed, timestamp_str
|
|
8
11
|
|
|
9
12
|
ensure_dirs()
|
|
10
13
|
logger = get_logger(__name__)
|
souleyez/security/__init__.py
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
"""Security validation, sanitization, and authentication utilities."""
|
|
2
2
|
|
|
3
|
-
import click
|
|
4
|
-
import sys
|
|
5
3
|
import functools
|
|
6
4
|
import getpass
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
|
|
7
9
|
from souleyez.storage.crypto import get_crypto_manager
|
|
8
10
|
|
|
9
11
|
|
|
@@ -20,7 +22,7 @@ def require_login(f):
|
|
|
20
22
|
|
|
21
23
|
@functools.wraps(f)
|
|
22
24
|
def wrapper(*args, **kwargs):
|
|
23
|
-
from souleyez.auth import get_current_user,
|
|
25
|
+
from souleyez.auth import get_current_user, init_auth, is_logged_in
|
|
24
26
|
from souleyez.storage.database import get_db
|
|
25
27
|
|
|
26
28
|
# Initialize auth if needed
|
|
@@ -56,7 +58,7 @@ def require_admin(f):
|
|
|
56
58
|
|
|
57
59
|
@functools.wraps(f)
|
|
58
60
|
def wrapper(*args, **kwargs):
|
|
59
|
-
from souleyez.auth import
|
|
61
|
+
from souleyez.auth import Role, get_current_user
|
|
60
62
|
|
|
61
63
|
user = get_current_user()
|
|
62
64
|
if user is None or user.role != Role.ADMIN:
|
|
@@ -83,7 +85,7 @@ def require_pro(f):
|
|
|
83
85
|
|
|
84
86
|
@functools.wraps(f)
|
|
85
87
|
def wrapper(*args, **kwargs):
|
|
86
|
-
from souleyez.auth import
|
|
88
|
+
from souleyez.auth import Tier, get_current_user
|
|
87
89
|
|
|
88
90
|
user = get_current_user()
|
|
89
91
|
if user is None or user.tier != Tier.PRO:
|
|
@@ -5,15 +5,16 @@ Engagement scope validation for target validation.
|
|
|
5
5
|
This module provides validation of targets against engagement scope definitions
|
|
6
6
|
to prevent scanning unauthorized targets.
|
|
7
7
|
"""
|
|
8
|
-
|
|
9
|
-
import ipaddress
|
|
8
|
+
|
|
10
9
|
import fnmatch
|
|
10
|
+
import ipaddress
|
|
11
|
+
import re
|
|
11
12
|
from dataclasses import dataclass
|
|
12
|
-
from typing import
|
|
13
|
+
from typing import Any, Dict, List, Optional
|
|
13
14
|
from urllib.parse import urlparse
|
|
14
15
|
|
|
15
|
-
from souleyez.storage.database import get_db
|
|
16
16
|
from souleyez.log_config import get_logger
|
|
17
|
+
from souleyez.storage.database import get_db
|
|
17
18
|
|
|
18
19
|
logger = get_logger(__name__)
|
|
19
20
|
|
souleyez/security.py
CHANGED
souleyez/storage/credentials.py
CHANGED
|
@@ -4,12 +4,15 @@ souleyez.storage.credentials - Credential storage and management
|
|
|
4
4
|
|
|
5
5
|
Similar to MSF's creds command - tracks enumerated usernames and discovered passwords.
|
|
6
6
|
"""
|
|
7
|
+
|
|
7
8
|
import re
|
|
8
|
-
from typing import
|
|
9
|
-
|
|
10
|
-
from .crypto import get_crypto_manager
|
|
9
|
+
from typing import Any, Dict, List, Optional
|
|
10
|
+
|
|
11
11
|
from souleyez.log_config import get_logger
|
|
12
12
|
|
|
13
|
+
from .crypto import get_crypto_manager
|
|
14
|
+
from .database import get_db
|
|
15
|
+
|
|
13
16
|
logger = get_logger(__name__)
|
|
14
17
|
|
|
15
18
|
|
|
@@ -77,8 +80,7 @@ class CredentialsManager:
|
|
|
77
80
|
def _ensure_table(self):
|
|
78
81
|
"""Ensure credentials table exists."""
|
|
79
82
|
conn = self.db.get_connection()
|
|
80
|
-
conn.execute(
|
|
81
|
-
"""
|
|
83
|
+
conn.execute("""
|
|
82
84
|
CREATE TABLE IF NOT EXISTS credentials (
|
|
83
85
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
84
86
|
engagement_id INTEGER NOT NULL,
|
|
@@ -95,28 +97,21 @@ class CredentialsManager:
|
|
|
95
97
|
FOREIGN KEY (engagement_id) REFERENCES engagements(id),
|
|
96
98
|
FOREIGN KEY (host_id) REFERENCES hosts(id)
|
|
97
99
|
)
|
|
98
|
-
"""
|
|
99
|
-
)
|
|
100
|
+
""")
|
|
100
101
|
|
|
101
102
|
# Create index for faster lookups
|
|
102
|
-
conn.execute(
|
|
103
|
-
"""
|
|
103
|
+
conn.execute("""
|
|
104
104
|
CREATE INDEX IF NOT EXISTS idx_credentials_engagement
|
|
105
105
|
ON credentials(engagement_id)
|
|
106
|
-
"""
|
|
107
|
-
|
|
108
|
-
conn.execute(
|
|
109
|
-
"""
|
|
106
|
+
""")
|
|
107
|
+
conn.execute("""
|
|
110
108
|
CREATE INDEX IF NOT EXISTS idx_credentials_host
|
|
111
109
|
ON credentials(host_id)
|
|
112
|
-
"""
|
|
113
|
-
|
|
114
|
-
conn.execute(
|
|
115
|
-
"""
|
|
110
|
+
""")
|
|
111
|
+
conn.execute("""
|
|
116
112
|
CREATE INDEX IF NOT EXISTS idx_credentials_status
|
|
117
113
|
ON credentials(status)
|
|
118
|
-
"""
|
|
119
|
-
)
|
|
114
|
+
""")
|
|
120
115
|
|
|
121
116
|
conn.commit()
|
|
122
117
|
conn.close()
|
souleyez/storage/crypto.py
CHANGED
|
@@ -5,17 +5,20 @@ souleyez.storage.crypto - Credential encryption/decryption
|
|
|
5
5
|
Provides transparent encryption for sensitive credential data using Fernet (symmetric encryption).
|
|
6
6
|
The encryption key is derived from a master password using PBKDF2-HMAC-SHA256.
|
|
7
7
|
"""
|
|
8
|
+
|
|
8
9
|
import base64
|
|
9
|
-
import os
|
|
10
10
|
import json
|
|
11
|
+
import os
|
|
11
12
|
import traceback
|
|
12
|
-
from typing import Optional
|
|
13
|
-
from pathlib import Path
|
|
14
13
|
from datetime import datetime, timedelta
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Optional
|
|
16
|
+
|
|
15
17
|
from cryptography.fernet import Fernet, InvalidToken
|
|
18
|
+
from cryptography.hazmat.backends import default_backend
|
|
16
19
|
from cryptography.hazmat.primitives import hashes
|
|
17
20
|
from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
|
|
18
|
-
|
|
21
|
+
|
|
19
22
|
from souleyez import config
|
|
20
23
|
from souleyez.log_config import get_logger
|
|
21
24
|
|
souleyez/storage/database.py
CHANGED
|
@@ -2,12 +2,14 @@
|
|
|
2
2
|
"""
|
|
3
3
|
souleyez.storage.database - Core database operations
|
|
4
4
|
"""
|
|
5
|
-
|
|
5
|
+
|
|
6
6
|
import os
|
|
7
|
+
import sqlite3
|
|
7
8
|
import time
|
|
8
9
|
import traceback
|
|
9
|
-
from typing import Optional, List, Dict, Any
|
|
10
10
|
from pathlib import Path
|
|
11
|
+
from typing import Any, Dict, List, Optional
|
|
12
|
+
|
|
11
13
|
from souleyez import config
|
|
12
14
|
from souleyez.log_config import get_logger
|
|
13
15
|
|
|
@@ -104,8 +106,7 @@ class Database:
|
|
|
104
106
|
"Schema file not found, using inline schema",
|
|
105
107
|
extra={"expected_path": str(schema_path)},
|
|
106
108
|
)
|
|
107
|
-
conn.executescript(
|
|
108
|
-
"""
|
|
109
|
+
conn.executescript("""
|
|
109
110
|
CREATE TABLE IF NOT EXISTS engagements (
|
|
110
111
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
111
112
|
name TEXT UNIQUE NOT NULL,
|
|
@@ -267,8 +268,7 @@ class Database:
|
|
|
267
268
|
CREATE INDEX IF NOT EXISTS idx_credentials_engagement ON credentials(engagement_id);
|
|
268
269
|
CREATE INDEX IF NOT EXISTS idx_credentials_host ON credentials(host_id);
|
|
269
270
|
CREATE INDEX IF NOT EXISTS idx_credentials_status ON credentials(status);
|
|
270
|
-
"""
|
|
271
|
-
)
|
|
271
|
+
""")
|
|
272
272
|
|
|
273
273
|
conn.commit()
|
|
274
274
|
conn.close()
|
souleyez/storage/db.py
CHANGED
|
@@ -10,12 +10,14 @@ Provides:
|
|
|
10
10
|
- get_scan(scan_id)
|
|
11
11
|
- simple migration helper to import old history JSON entries
|
|
12
12
|
"""
|
|
13
|
-
|
|
14
|
-
import sqlite3
|
|
13
|
+
|
|
15
14
|
import json
|
|
16
|
-
|
|
17
|
-
from
|
|
15
|
+
import sqlite3
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any, Dict, List, Optional
|
|
18
|
+
|
|
18
19
|
from .. import config
|
|
20
|
+
from ..utils import HISTORY_FILE, ensure_dirs, read_json
|
|
19
21
|
|
|
20
22
|
DB_PATH = Path(config.get("database.path", "~/.souleyez/souleyez.db")).expanduser()
|
|
21
23
|
ensure_dirs()
|
|
@@ -41,8 +43,7 @@ def _get_conn(fast_mode: bool = False):
|
|
|
41
43
|
def init_db():
|
|
42
44
|
conn = _get_conn()
|
|
43
45
|
cur = conn.cursor()
|
|
44
|
-
cur.executescript(
|
|
45
|
-
"""
|
|
46
|
+
cur.executescript("""
|
|
46
47
|
CREATE TABLE IF NOT EXISTS scans (
|
|
47
48
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
48
49
|
ts TEXT,
|
|
@@ -60,8 +61,7 @@ def init_db():
|
|
|
60
61
|
CREATE INDEX IF NOT EXISTS idx_scans_tool ON scans(tool);
|
|
61
62
|
CREATE INDEX IF NOT EXISTS idx_scans_target ON scans(target);
|
|
62
63
|
CREATE INDEX IF NOT EXISTS idx_scans_ts ON scans(ts);
|
|
63
|
-
"""
|
|
64
|
-
)
|
|
64
|
+
""")
|
|
65
65
|
conn.commit()
|
|
66
66
|
conn.close()
|
|
67
67
|
|
|
@@ -4,11 +4,12 @@ Export deliverables in multiple formats (CSV, JSON, Markdown).
|
|
|
4
4
|
|
|
5
5
|
import csv
|
|
6
6
|
import json
|
|
7
|
-
from typing import Dict, List, Optional
|
|
8
7
|
from datetime import datetime
|
|
8
|
+
from typing import Dict, List, Optional
|
|
9
|
+
|
|
9
10
|
from .database import get_db
|
|
10
|
-
from .deliverables import DeliverableManager
|
|
11
11
|
from .deliverable_evidence import EvidenceManager
|
|
12
|
+
from .deliverables import DeliverableManager
|
|
12
13
|
from .engagements import EngagementManager
|
|
13
14
|
|
|
14
15
|
|
souleyez/storage/deliverables.py
CHANGED
souleyez/storage/engagements.py
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
"""
|
|
3
3
|
souleyez.storage.engagements - Engagement management
|
|
4
4
|
"""
|
|
5
|
-
|
|
6
|
-
from .database import get_db
|
|
5
|
+
|
|
7
6
|
from pathlib import Path
|
|
7
|
+
from typing import Any, Dict, List, Optional
|
|
8
|
+
|
|
9
|
+
from .database import get_db
|
|
8
10
|
|
|
9
11
|
ENGAGEMENT_FILE = Path.home() / ".souleyez" / "current_engagement"
|
|
10
12
|
|
|
@@ -39,7 +41,7 @@ class EngagementManager:
|
|
|
39
41
|
)
|
|
40
42
|
|
|
41
43
|
# Audit log
|
|
42
|
-
from souleyez.auth.audit import
|
|
44
|
+
from souleyez.auth.audit import AuditAction, audit_log
|
|
43
45
|
|
|
44
46
|
audit_log(
|
|
45
47
|
AuditAction.ENGAGEMENT_CREATED,
|
|
@@ -241,7 +243,7 @@ class EngagementManager:
|
|
|
241
243
|
self.db.execute("DELETE FROM engagements WHERE id = ?", (eng["id"],))
|
|
242
244
|
|
|
243
245
|
# Audit log
|
|
244
|
-
from souleyez.auth.audit import
|
|
246
|
+
from souleyez.auth.audit import AuditAction, audit_log
|
|
245
247
|
|
|
246
248
|
audit_log(
|
|
247
249
|
AuditAction.ENGAGEMENT_DELETED,
|
souleyez/storage/evidence.py
CHANGED
|
@@ -3,8 +3,9 @@
|
|
|
3
3
|
Evidence collection and organization for pentest workflow.
|
|
4
4
|
Aggregates all artifacts from jobs, findings, credentials, etc.
|
|
5
5
|
"""
|
|
6
|
-
|
|
6
|
+
|
|
7
7
|
from datetime import datetime, timedelta
|
|
8
|
+
from typing import Dict, List, Optional
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
class EvidenceManager:
|
|
@@ -12,10 +13,10 @@ class EvidenceManager:
|
|
|
12
13
|
|
|
13
14
|
def __init__(self):
|
|
14
15
|
# Import existing managers
|
|
15
|
-
from souleyez.storage.osint import OsintManager
|
|
16
|
-
from souleyez.storage.hosts import HostManager
|
|
17
|
-
from souleyez.storage.findings import FindingsManager
|
|
18
16
|
from souleyez.storage.credentials import CredentialsManager
|
|
17
|
+
from souleyez.storage.findings import FindingsManager
|
|
18
|
+
from souleyez.storage.hosts import HostManager
|
|
19
|
+
from souleyez.storage.osint import OsintManager
|
|
19
20
|
from souleyez.storage.screenshots import ScreenshotManager
|
|
20
21
|
|
|
21
22
|
self.osint = OsintManager()
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
"""
|
|
3
3
|
souleyez.storage.execution_log - Track AI-driven command executions
|
|
4
4
|
"""
|
|
5
|
-
|
|
5
|
+
|
|
6
6
|
import json
|
|
7
|
+
import sqlite3
|
|
7
8
|
from datetime import datetime
|
|
8
|
-
from typing import Dict, List, Optional, Any
|
|
9
9
|
from pathlib import Path
|
|
10
|
+
from typing import Any, Dict, List, Optional
|
|
11
|
+
|
|
10
12
|
from souleyez import config
|
|
11
13
|
|
|
12
14
|
|
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
"""
|
|
3
3
|
souleyez.storage.exploit_attempts - Track exploitation attempts and outcomes
|
|
4
4
|
"""
|
|
5
|
-
|
|
5
|
+
|
|
6
6
|
from datetime import datetime
|
|
7
|
-
from
|
|
7
|
+
from typing import Any, Dict, List, Optional
|
|
8
8
|
|
|
9
|
+
from souleyez.storage.database import get_db
|
|
9
10
|
|
|
10
11
|
# Valid status values
|
|
11
12
|
STATUS_NOT_TRIED = "not_tried"
|
souleyez/storage/exploits.py
CHANGED
souleyez/storage/findings.py
CHANGED
souleyez/storage/hosts.py
CHANGED
|
@@ -2,10 +2,13 @@
|
|
|
2
2
|
"""
|
|
3
3
|
souleyez.storage.hosts - Host and service management
|
|
4
4
|
"""
|
|
5
|
-
|
|
6
|
-
from
|
|
5
|
+
|
|
6
|
+
from typing import Any, Dict, List, Optional
|
|
7
|
+
|
|
7
8
|
from souleyez.log_config import get_logger
|
|
8
9
|
|
|
10
|
+
from .database import get_db
|
|
11
|
+
|
|
9
12
|
logger = get_logger(__name__)
|
|
10
13
|
|
|
11
14
|
|