souleyez 2.43.29__py3-none-any.whl → 2.43.34__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.
- souleyez/__init__.py +1 -2
- souleyez/ai/__init__.py +21 -15
- souleyez/ai/action_mapper.py +249 -150
- souleyez/ai/chain_advisor.py +116 -100
- souleyez/ai/claude_provider.py +29 -28
- souleyez/ai/context_builder.py +80 -62
- souleyez/ai/executor.py +158 -117
- souleyez/ai/feedback_handler.py +136 -121
- souleyez/ai/llm_factory.py +27 -20
- souleyez/ai/llm_provider.py +4 -2
- souleyez/ai/ollama_provider.py +6 -9
- souleyez/ai/ollama_service.py +44 -37
- souleyez/ai/path_scorer.py +91 -76
- souleyez/ai/recommender.py +176 -144
- souleyez/ai/report_context.py +74 -73
- souleyez/ai/report_service.py +84 -66
- souleyez/ai/result_parser.py +222 -229
- souleyez/ai/safety.py +67 -44
- souleyez/auth/__init__.py +23 -22
- souleyez/auth/audit.py +36 -26
- souleyez/auth/engagement_access.py +65 -48
- souleyez/auth/permissions.py +14 -3
- souleyez/auth/session_manager.py +54 -37
- souleyez/auth/user_manager.py +109 -64
- souleyez/commands/audit.py +40 -43
- souleyez/commands/auth.py +35 -15
- souleyez/commands/deliverables.py +55 -50
- souleyez/commands/engagement.py +47 -28
- souleyez/commands/license.py +32 -23
- souleyez/commands/screenshots.py +36 -32
- souleyez/commands/user.py +82 -36
- souleyez/config.py +52 -44
- souleyez/core/credential_tester.py +87 -81
- souleyez/core/cve_mappings.py +179 -192
- souleyez/core/cve_matcher.py +162 -148
- souleyez/core/msf_auto_mapper.py +100 -83
- souleyez/core/msf_chain_engine.py +294 -256
- souleyez/core/msf_database.py +153 -70
- souleyez/core/msf_integration.py +679 -673
- souleyez/core/msf_rpc_client.py +40 -42
- souleyez/core/msf_rpc_manager.py +77 -79
- souleyez/core/msf_sync_manager.py +241 -181
- souleyez/core/network_utils.py +22 -15
- souleyez/core/parser_handler.py +34 -25
- souleyez/core/pending_chains.py +114 -63
- souleyez/core/templates.py +158 -107
- souleyez/core/tool_chaining.py +9526 -2879
- souleyez/core/version_utils.py +79 -94
- souleyez/core/vuln_correlation.py +136 -89
- souleyez/core/web_utils.py +33 -32
- souleyez/data/wordlists/ad_users.txt +378 -0
- souleyez/data/wordlists/api_endpoints_large.txt +769 -0
- souleyez/data/wordlists/home_dir_sensitive.txt +39 -0
- souleyez/data/wordlists/lfi_payloads.txt +82 -0
- souleyez/data/wordlists/passwords_brute.txt +1548 -0
- souleyez/data/wordlists/passwords_crack.txt +2479 -0
- souleyez/data/wordlists/passwords_spray.txt +386 -0
- souleyez/data/wordlists/subdomains_large.txt +5057 -0
- souleyez/data/wordlists/usernames_common.txt +694 -0
- souleyez/data/wordlists/web_dirs_large.txt +4769 -0
- souleyez/detection/__init__.py +1 -1
- souleyez/detection/attack_signatures.py +12 -17
- souleyez/detection/mitre_mappings.py +61 -55
- souleyez/detection/validator.py +97 -86
- souleyez/devtools.py +23 -10
- souleyez/docs/README.md +4 -4
- souleyez/docs/api-reference/cli-commands.md +2 -2
- souleyez/docs/developer-guide/adding-new-tools.md +562 -0
- souleyez/docs/user-guide/auto-chaining.md +30 -8
- souleyez/docs/user-guide/getting-started.md +1 -1
- souleyez/docs/user-guide/installation.md +26 -3
- souleyez/docs/user-guide/metasploit-integration.md +2 -2
- souleyez/docs/user-guide/rbac.md +1 -1
- souleyez/docs/user-guide/scope-management.md +1 -1
- souleyez/docs/user-guide/siem-integration.md +1 -1
- souleyez/docs/user-guide/tools-reference.md +1 -8
- souleyez/docs/user-guide/worker-management.md +1 -1
- souleyez/engine/background.py +1239 -535
- souleyez/engine/base.py +4 -1
- souleyez/engine/job_status.py +17 -49
- souleyez/engine/log_sanitizer.py +103 -77
- souleyez/engine/manager.py +38 -7
- souleyez/engine/result_handler.py +2200 -1550
- souleyez/engine/worker_manager.py +50 -41
- souleyez/export/evidence_bundle.py +72 -62
- souleyez/feature_flags/features.py +16 -20
- souleyez/feature_flags.py +5 -9
- souleyez/handlers/__init__.py +11 -0
- souleyez/handlers/base.py +188 -0
- souleyez/handlers/bash_handler.py +277 -0
- souleyez/handlers/bloodhound_handler.py +243 -0
- souleyez/handlers/certipy_handler.py +311 -0
- souleyez/handlers/crackmapexec_handler.py +486 -0
- souleyez/handlers/dnsrecon_handler.py +344 -0
- souleyez/handlers/enum4linux_handler.py +400 -0
- souleyez/handlers/evil_winrm_handler.py +493 -0
- souleyez/handlers/ffuf_handler.py +815 -0
- souleyez/handlers/gobuster_handler.py +1114 -0
- souleyez/handlers/gpp_extract_handler.py +334 -0
- souleyez/handlers/hashcat_handler.py +444 -0
- souleyez/handlers/hydra_handler.py +563 -0
- souleyez/handlers/impacket_getuserspns_handler.py +343 -0
- souleyez/handlers/impacket_psexec_handler.py +222 -0
- souleyez/handlers/impacket_secretsdump_handler.py +426 -0
- souleyez/handlers/john_handler.py +286 -0
- souleyez/handlers/katana_handler.py +425 -0
- souleyez/handlers/kerbrute_handler.py +298 -0
- souleyez/handlers/ldapsearch_handler.py +636 -0
- souleyez/handlers/lfi_extract_handler.py +464 -0
- souleyez/handlers/msf_auxiliary_handler.py +408 -0
- souleyez/handlers/msf_exploit_handler.py +380 -0
- souleyez/handlers/nikto_handler.py +413 -0
- souleyez/handlers/nmap_handler.py +821 -0
- souleyez/handlers/nuclei_handler.py +359 -0
- souleyez/handlers/nxc_handler.py +371 -0
- souleyez/handlers/rdp_sec_check_handler.py +353 -0
- souleyez/handlers/registry.py +292 -0
- souleyez/handlers/responder_handler.py +232 -0
- souleyez/handlers/service_explorer_handler.py +434 -0
- souleyez/handlers/smbclient_handler.py +344 -0
- souleyez/handlers/smbmap_handler.py +510 -0
- souleyez/handlers/smbpasswd_handler.py +296 -0
- souleyez/handlers/sqlmap_handler.py +1116 -0
- souleyez/handlers/theharvester_handler.py +601 -0
- souleyez/handlers/web_login_test_handler.py +327 -0
- souleyez/handlers/whois_handler.py +277 -0
- souleyez/handlers/wpscan_handler.py +554 -0
- souleyez/history.py +32 -16
- souleyez/importers/msf_importer.py +106 -75
- souleyez/importers/smart_importer.py +208 -147
- souleyez/integrations/siem/__init__.py +10 -10
- souleyez/integrations/siem/base.py +17 -18
- souleyez/integrations/siem/elastic.py +108 -122
- souleyez/integrations/siem/factory.py +207 -80
- souleyez/integrations/siem/googlesecops.py +146 -154
- souleyez/integrations/siem/rule_mappings/__init__.py +1 -1
- souleyez/integrations/siem/rule_mappings/wazuh_rules.py +8 -5
- souleyez/integrations/siem/sentinel.py +107 -109
- souleyez/integrations/siem/splunk.py +246 -212
- souleyez/integrations/siem/wazuh.py +65 -71
- souleyez/integrations/wazuh/__init__.py +5 -5
- souleyez/integrations/wazuh/client.py +70 -93
- souleyez/integrations/wazuh/config.py +85 -57
- souleyez/integrations/wazuh/host_mapper.py +28 -36
- souleyez/integrations/wazuh/sync.py +78 -68
- souleyez/intelligence/__init__.py +4 -5
- souleyez/intelligence/correlation_analyzer.py +309 -295
- souleyez/intelligence/exploit_knowledge.py +661 -623
- souleyez/intelligence/exploit_suggestions.py +159 -139
- souleyez/intelligence/gap_analyzer.py +132 -97
- souleyez/intelligence/gap_detector.py +251 -214
- souleyez/intelligence/sensitive_tables.py +266 -129
- souleyez/intelligence/service_parser.py +137 -123
- souleyez/intelligence/surface_analyzer.py +407 -268
- souleyez/intelligence/target_parser.py +159 -162
- souleyez/licensing/__init__.py +6 -6
- souleyez/licensing/validator.py +17 -19
- souleyez/log_config.py +79 -54
- souleyez/main.py +1505 -687
- souleyez/migrations/fix_job_counter.py +16 -14
- souleyez/parsers/bloodhound_parser.py +41 -39
- souleyez/parsers/crackmapexec_parser.py +178 -111
- souleyez/parsers/dalfox_parser.py +72 -77
- souleyez/parsers/dnsrecon_parser.py +103 -91
- souleyez/parsers/enum4linux_parser.py +183 -153
- souleyez/parsers/ffuf_parser.py +29 -25
- souleyez/parsers/gobuster_parser.py +301 -41
- souleyez/parsers/hashcat_parser.py +324 -79
- souleyez/parsers/http_fingerprint_parser.py +350 -103
- souleyez/parsers/hydra_parser.py +131 -111
- souleyez/parsers/impacket_parser.py +231 -178
- souleyez/parsers/john_parser.py +98 -86
- souleyez/parsers/katana_parser.py +316 -0
- souleyez/parsers/msf_parser.py +943 -498
- souleyez/parsers/nikto_parser.py +346 -65
- souleyez/parsers/nmap_parser.py +262 -174
- souleyez/parsers/nuclei_parser.py +40 -44
- souleyez/parsers/responder_parser.py +26 -26
- souleyez/parsers/searchsploit_parser.py +74 -74
- souleyez/parsers/service_explorer_parser.py +279 -0
- souleyez/parsers/smbmap_parser.py +180 -124
- souleyez/parsers/sqlmap_parser.py +434 -308
- souleyez/parsers/theharvester_parser.py +75 -57
- souleyez/parsers/whois_parser.py +135 -94
- souleyez/parsers/wpscan_parser.py +278 -190
- souleyez/plugins/afp.py +44 -36
- souleyez/plugins/afp_brute.py +114 -46
- souleyez/plugins/ard.py +48 -37
- souleyez/plugins/bloodhound.py +95 -61
- souleyez/plugins/certipy.py +303 -0
- souleyez/plugins/crackmapexec.py +186 -85
- souleyez/plugins/dalfox.py +120 -59
- souleyez/plugins/dns_hijack.py +146 -41
- souleyez/plugins/dnsrecon.py +97 -61
- souleyez/plugins/enum4linux.py +91 -66
- souleyez/plugins/evil_winrm.py +291 -0
- souleyez/plugins/ffuf.py +166 -90
- souleyez/plugins/firmware_extract.py +133 -29
- souleyez/plugins/gobuster.py +387 -190
- souleyez/plugins/gpp_extract.py +393 -0
- souleyez/plugins/hashcat.py +100 -73
- souleyez/plugins/http_fingerprint.py +854 -267
- souleyez/plugins/hydra.py +566 -200
- souleyez/plugins/impacket_getnpusers.py +117 -69
- souleyez/plugins/impacket_psexec.py +84 -64
- souleyez/plugins/impacket_secretsdump.py +103 -69
- souleyez/plugins/impacket_smbclient.py +89 -75
- souleyez/plugins/john.py +86 -69
- souleyez/plugins/katana.py +313 -0
- souleyez/plugins/kerbrute.py +237 -0
- souleyez/plugins/lfi_extract.py +541 -0
- souleyez/plugins/macos_ssh.py +117 -48
- souleyez/plugins/mdns.py +35 -30
- souleyez/plugins/msf_auxiliary.py +253 -130
- souleyez/plugins/msf_exploit.py +239 -161
- souleyez/plugins/nikto.py +134 -78
- souleyez/plugins/nmap.py +275 -91
- souleyez/plugins/nuclei.py +180 -89
- souleyez/plugins/nxc.py +285 -0
- souleyez/plugins/plugin_base.py +35 -36
- souleyez/plugins/plugin_template.py +13 -5
- souleyez/plugins/rdp_sec_check.py +130 -0
- souleyez/plugins/responder.py +112 -71
- souleyez/plugins/router_http_brute.py +76 -65
- souleyez/plugins/router_ssh_brute.py +118 -41
- souleyez/plugins/router_telnet_brute.py +124 -42
- souleyez/plugins/routersploit.py +91 -59
- souleyez/plugins/routersploit_exploit.py +77 -55
- souleyez/plugins/searchsploit.py +91 -77
- souleyez/plugins/service_explorer.py +1160 -0
- souleyez/plugins/smbmap.py +122 -72
- souleyez/plugins/smbpasswd.py +215 -0
- souleyez/plugins/sqlmap.py +301 -113
- souleyez/plugins/theharvester.py +127 -75
- souleyez/plugins/tr069.py +79 -57
- souleyez/plugins/upnp.py +65 -47
- souleyez/plugins/upnp_abuse.py +73 -55
- souleyez/plugins/vnc_access.py +129 -42
- souleyez/plugins/vnc_brute.py +109 -38
- souleyez/plugins/web_login_test.py +417 -0
- souleyez/plugins/whois.py +77 -58
- souleyez/plugins/wpscan.py +173 -69
- souleyez/reporting/__init__.py +2 -1
- souleyez/reporting/attack_chain.py +411 -346
- souleyez/reporting/charts.py +436 -501
- souleyez/reporting/compliance_mappings.py +334 -201
- souleyez/reporting/detection_report.py +126 -125
- souleyez/reporting/formatters.py +828 -591
- souleyez/reporting/generator.py +386 -302
- souleyez/reporting/metrics.py +72 -75
- souleyez/scanner.py +35 -29
- souleyez/security/__init__.py +37 -11
- souleyez/security/scope_validator.py +175 -106
- souleyez/security/validation.py +223 -149
- souleyez/security.py +22 -6
- souleyez/storage/credentials.py +247 -186
- souleyez/storage/crypto.py +296 -129
- souleyez/storage/database.py +73 -50
- souleyez/storage/db.py +58 -36
- souleyez/storage/deliverable_evidence.py +177 -128
- souleyez/storage/deliverable_exporter.py +282 -246
- souleyez/storage/deliverable_templates.py +134 -116
- souleyez/storage/deliverables.py +135 -130
- souleyez/storage/engagements.py +109 -56
- souleyez/storage/evidence.py +181 -152
- souleyez/storage/execution_log.py +31 -17
- souleyez/storage/exploit_attempts.py +93 -57
- souleyez/storage/exploits.py +67 -36
- souleyez/storage/findings.py +48 -61
- souleyez/storage/hosts.py +176 -144
- souleyez/storage/migrate_to_engagements.py +43 -19
- souleyez/storage/migrations/_001_add_credential_enhancements.py +22 -12
- souleyez/storage/migrations/_002_add_status_tracking.py +10 -7
- souleyez/storage/migrations/_003_add_execution_log.py +14 -8
- souleyez/storage/migrations/_005_screenshots.py +13 -5
- souleyez/storage/migrations/_006_deliverables.py +13 -5
- souleyez/storage/migrations/_007_deliverable_templates.py +12 -7
- souleyez/storage/migrations/_008_add_nuclei_table.py +10 -4
- souleyez/storage/migrations/_010_evidence_linking.py +17 -10
- souleyez/storage/migrations/_011_timeline_tracking.py +20 -13
- souleyez/storage/migrations/_012_team_collaboration.py +34 -21
- souleyez/storage/migrations/_013_add_host_tags.py +12 -6
- souleyez/storage/migrations/_014_exploit_attempts.py +22 -10
- souleyez/storage/migrations/_015_add_mac_os_fields.py +15 -7
- souleyez/storage/migrations/_016_add_domain_field.py +10 -4
- souleyez/storage/migrations/_017_msf_sessions.py +16 -8
- souleyez/storage/migrations/_018_add_osint_target.py +10 -6
- souleyez/storage/migrations/_019_add_engagement_type.py +10 -6
- souleyez/storage/migrations/_020_add_rbac.py +36 -15
- souleyez/storage/migrations/_021_wazuh_integration.py +20 -8
- souleyez/storage/migrations/_022_wazuh_indexer_columns.py +6 -4
- souleyez/storage/migrations/_023_fix_detection_results_fk.py +16 -6
- souleyez/storage/migrations/_024_wazuh_vulnerabilities.py +26 -10
- souleyez/storage/migrations/_025_multi_siem_support.py +3 -5
- souleyez/storage/migrations/_026_add_engagement_scope.py +31 -12
- souleyez/storage/migrations/_027_multi_siem_persistence.py +32 -15
- souleyez/storage/migrations/__init__.py +26 -26
- souleyez/storage/migrations/migration_manager.py +19 -19
- souleyez/storage/msf_sessions.py +100 -65
- souleyez/storage/osint.py +17 -24
- souleyez/storage/recommendation_engine.py +269 -235
- souleyez/storage/screenshots.py +33 -32
- souleyez/storage/smb_shares.py +136 -92
- souleyez/storage/sqlmap_data.py +183 -128
- souleyez/storage/team_collaboration.py +135 -141
- souleyez/storage/timeline_tracker.py +122 -94
- souleyez/storage/wazuh_vulns.py +64 -66
- souleyez/storage/web_paths.py +33 -37
- souleyez/testing/credential_tester.py +221 -205
- souleyez/ui/__init__.py +1 -1
- souleyez/ui/ai_quotes.py +12 -12
- souleyez/ui/attack_surface.py +2439 -1516
- souleyez/ui/chain_rules_view.py +914 -382
- souleyez/ui/correlation_view.py +312 -230
- souleyez/ui/dashboard.py +2382 -1130
- souleyez/ui/deliverables_view.py +148 -62
- souleyez/ui/design_system.py +13 -13
- souleyez/ui/errors.py +49 -49
- souleyez/ui/evidence_linking_view.py +284 -179
- souleyez/ui/evidence_vault.py +393 -285
- souleyez/ui/exploit_suggestions_view.py +555 -349
- souleyez/ui/export_view.py +100 -66
- souleyez/ui/gap_analysis_view.py +315 -171
- souleyez/ui/help_system.py +105 -97
- souleyez/ui/intelligence_view.py +436 -293
- souleyez/ui/interactive.py +22827 -10678
- souleyez/ui/interactive_selector.py +75 -68
- souleyez/ui/log_formatter.py +47 -39
- souleyez/ui/menu_components.py +22 -13
- souleyez/ui/msf_auxiliary_menu.py +184 -133
- souleyez/ui/pending_chains_view.py +336 -172
- souleyez/ui/progress_indicators.py +5 -3
- souleyez/ui/recommendations_view.py +195 -137
- souleyez/ui/rule_builder.py +343 -225
- souleyez/ui/setup_wizard.py +678 -284
- souleyez/ui/shortcuts.py +217 -165
- souleyez/ui/splunk_gap_analysis_view.py +452 -270
- souleyez/ui/splunk_vulns_view.py +139 -86
- souleyez/ui/team_dashboard.py +498 -335
- souleyez/ui/template_selector.py +196 -105
- souleyez/ui/terminal.py +6 -6
- souleyez/ui/timeline_view.py +198 -127
- souleyez/ui/tool_setup.py +264 -164
- souleyez/ui/tutorial.py +202 -72
- souleyez/ui/tutorial_state.py +40 -40
- souleyez/ui/wazuh_vulns_view.py +235 -141
- souleyez/ui/wordlist_browser.py +260 -107
- souleyez/ui.py +464 -312
- souleyez/utils/tool_checker.py +427 -367
- souleyez/utils.py +33 -29
- souleyez/wordlists.py +134 -167
- {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/METADATA +1 -1
- souleyez-2.43.34.dist-info/RECORD +443 -0
- {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/WHEEL +1 -1
- souleyez-2.43.29.dist-info/RECORD +0 -379
- {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/entry_points.txt +0 -0
- {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/licenses/LICENSE +0 -0
- {souleyez-2.43.29.dist-info → souleyez-2.43.34.dist-info}/top_level.txt +0 -0
|
@@ -13,12 +13,12 @@ class SmartImporter:
|
|
|
13
13
|
|
|
14
14
|
def __init__(self):
|
|
15
15
|
self.detected_types = {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
16
|
+
"hosts": 0,
|
|
17
|
+
"services": 0,
|
|
18
|
+
"vulnerabilities": 0,
|
|
19
|
+
"credentials": 0,
|
|
20
|
+
"web_paths": 0,
|
|
21
|
+
"notes": 0,
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
def analyze_msf_xml(self, xml_path: str) -> Dict[str, Any]:
|
|
@@ -33,103 +33,135 @@ class SmartImporter:
|
|
|
33
33
|
root = tree.getroot()
|
|
34
34
|
|
|
35
35
|
analysis = {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
36
|
+
"file_type": "msf_xml",
|
|
37
|
+
"hosts": {"count": 0, "preview": []},
|
|
38
|
+
"services": {"count": 0, "preview": []},
|
|
39
|
+
"vulnerabilities": {"count": 0, "preview": []},
|
|
40
|
+
"credentials": {"count": 0, "preview": []},
|
|
41
|
+
"web_paths": {"count": 0, "preview": []},
|
|
42
|
+
"notes": {"count": 0, "preview": []},
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
# Analyze hosts
|
|
46
|
-
hosts = root.findall(
|
|
47
|
-
analysis[
|
|
46
|
+
hosts = root.findall(".//host")
|
|
47
|
+
analysis["hosts"]["count"] = len(hosts)
|
|
48
48
|
for host in hosts[:3]: # Preview first 3
|
|
49
|
-
address = host.find(
|
|
50
|
-
name = host.find(
|
|
51
|
-
os_name = host.find(
|
|
52
|
-
analysis[
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
49
|
+
address = host.find("address")
|
|
50
|
+
name = host.find("name")
|
|
51
|
+
os_name = host.find("os-name")
|
|
52
|
+
analysis["hosts"]["preview"].append(
|
|
53
|
+
{
|
|
54
|
+
"address": address.text if address is not None else "",
|
|
55
|
+
"name": name.text if name is not None else "",
|
|
56
|
+
"os": os_name.text if os_name is not None else "",
|
|
57
|
+
}
|
|
58
|
+
)
|
|
57
59
|
|
|
58
60
|
# Analyze services
|
|
59
|
-
services = root.findall(
|
|
60
|
-
analysis[
|
|
61
|
+
services = root.findall(".//service")
|
|
62
|
+
analysis["services"]["count"] = len(services)
|
|
61
63
|
for svc in services[:3]:
|
|
62
|
-
port = svc.find(
|
|
63
|
-
proto = svc.find(
|
|
64
|
-
name = svc.find(
|
|
65
|
-
info = svc.find(
|
|
66
|
-
analysis[
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
64
|
+
port = svc.find("port")
|
|
65
|
+
proto = svc.find("proto")
|
|
66
|
+
name = svc.find("name")
|
|
67
|
+
info = svc.find("info")
|
|
68
|
+
analysis["services"]["preview"].append(
|
|
69
|
+
{
|
|
70
|
+
"port": port.text if port is not None else "",
|
|
71
|
+
"proto": proto.text if proto is not None else "",
|
|
72
|
+
"name": name.text if name is not None else "",
|
|
73
|
+
"info": info.text if info is not None else "",
|
|
74
|
+
}
|
|
75
|
+
)
|
|
72
76
|
|
|
73
77
|
# Analyze vulnerabilities (notes with vuln refs)
|
|
74
|
-
notes = root.findall(
|
|
78
|
+
notes = root.findall(".//note")
|
|
75
79
|
for note in notes:
|
|
76
|
-
ntype = note.find(
|
|
77
|
-
data = note.find(
|
|
80
|
+
ntype = note.find("ntype")
|
|
81
|
+
data = note.find("data")
|
|
78
82
|
|
|
79
83
|
if ntype is not None and data is not None:
|
|
80
84
|
# Check if it's a vulnerability
|
|
81
|
-
if
|
|
82
|
-
analysis[
|
|
83
|
-
if len(analysis[
|
|
84
|
-
analysis[
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
85
|
+
if "vuln" in ntype.text.lower() or self._looks_like_vuln(data.text):
|
|
86
|
+
analysis["vulnerabilities"]["count"] += 1
|
|
87
|
+
if len(analysis["vulnerabilities"]["preview"]) < 3:
|
|
88
|
+
analysis["vulnerabilities"]["preview"].append(
|
|
89
|
+
{
|
|
90
|
+
"type": ntype.text,
|
|
91
|
+
"data": (
|
|
92
|
+
data.text[:100] + "..."
|
|
93
|
+
if len(data.text) > 100
|
|
94
|
+
else data.text
|
|
95
|
+
),
|
|
96
|
+
}
|
|
97
|
+
)
|
|
88
98
|
else:
|
|
89
|
-
analysis[
|
|
90
|
-
if len(analysis[
|
|
91
|
-
analysis[
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
99
|
+
analysis["notes"]["count"] += 1
|
|
100
|
+
if len(analysis["notes"]["preview"]) < 3:
|
|
101
|
+
analysis["notes"]["preview"].append(
|
|
102
|
+
{
|
|
103
|
+
"type": ntype.text,
|
|
104
|
+
"data": (
|
|
105
|
+
data.text[:100] + "..."
|
|
106
|
+
if len(data.text) > 100
|
|
107
|
+
else data.text
|
|
108
|
+
),
|
|
109
|
+
}
|
|
110
|
+
)
|
|
95
111
|
|
|
96
112
|
# Analyze credentials
|
|
97
|
-
creds = root.findall(
|
|
98
|
-
analysis[
|
|
113
|
+
creds = root.findall(".//cred")
|
|
114
|
+
analysis["credentials"]["count"] = len(creds)
|
|
99
115
|
for cred in creds[:3]:
|
|
100
|
-
user = cred.find(
|
|
101
|
-
pass_elem = cred.find(
|
|
102
|
-
service = cred.find(
|
|
103
|
-
analysis[
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
116
|
+
user = cred.find("user")
|
|
117
|
+
pass_elem = cred.find("pass")
|
|
118
|
+
service = cred.find("service")
|
|
119
|
+
analysis["credentials"]["preview"].append(
|
|
120
|
+
{
|
|
121
|
+
"username": user.text if user is not None else "",
|
|
122
|
+
"password": (
|
|
123
|
+
pass_elem.text[:20] + "..."
|
|
124
|
+
if pass_elem is not None and len(pass_elem.text) > 20
|
|
125
|
+
else (pass_elem.text if pass_elem is not None else "")
|
|
126
|
+
),
|
|
127
|
+
"service": service.text if service is not None else "",
|
|
128
|
+
}
|
|
129
|
+
)
|
|
108
130
|
|
|
109
131
|
# Analyze web paths/findings
|
|
110
|
-
web_vulns = root.findall(
|
|
111
|
-
analysis[
|
|
132
|
+
web_vulns = root.findall(".//web_vuln")
|
|
133
|
+
analysis["web_paths"]["count"] = len(web_vulns)
|
|
112
134
|
for wv in web_vulns[:3]:
|
|
113
|
-
path = wv.find(
|
|
114
|
-
method = wv.find(
|
|
115
|
-
pname = wv.find(
|
|
116
|
-
analysis[
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
135
|
+
path = wv.find("path")
|
|
136
|
+
method = wv.find("method")
|
|
137
|
+
pname = wv.find("pname")
|
|
138
|
+
analysis["web_paths"]["preview"].append(
|
|
139
|
+
{
|
|
140
|
+
"path": path.text if path is not None else "",
|
|
141
|
+
"method": method.text if method is not None else "",
|
|
142
|
+
"param": pname.text if pname is not None else "",
|
|
143
|
+
}
|
|
144
|
+
)
|
|
121
145
|
|
|
122
146
|
return analysis
|
|
123
147
|
|
|
124
148
|
except Exception as e:
|
|
125
|
-
return {
|
|
149
|
+
return {"error": str(e)}
|
|
126
150
|
|
|
127
151
|
def _looks_like_vuln(self, text: str) -> bool:
|
|
128
152
|
"""Check if text looks like vulnerability data."""
|
|
129
153
|
vuln_keywords = [
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
154
|
+
"cve-",
|
|
155
|
+
"exploit",
|
|
156
|
+
"vulnerable",
|
|
157
|
+
"vulnerability",
|
|
158
|
+
"attack",
|
|
159
|
+
"injection",
|
|
160
|
+
"xss",
|
|
161
|
+
"sqli",
|
|
162
|
+
"rce",
|
|
163
|
+
"buffer overflow",
|
|
164
|
+
"authentication bypass",
|
|
133
165
|
]
|
|
134
166
|
text_lower = text.lower()
|
|
135
167
|
return any(keyword in text_lower for keyword in vuln_keywords)
|
|
@@ -142,8 +174,9 @@ class SmartImporter:
|
|
|
142
174
|
"""
|
|
143
175
|
return self.analyze_msf_xml(xml_path)
|
|
144
176
|
|
|
145
|
-
def selective_import(
|
|
146
|
-
|
|
177
|
+
def selective_import(
|
|
178
|
+
self, xml_path: str, engagement_id: int, import_types: List[str]
|
|
179
|
+
) -> Dict[str, int]:
|
|
147
180
|
"""
|
|
148
181
|
Import only selected data types.
|
|
149
182
|
|
|
@@ -161,12 +194,12 @@ class SmartImporter:
|
|
|
161
194
|
from souleyez.storage.credentials import CredentialsManager
|
|
162
195
|
|
|
163
196
|
results = {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
197
|
+
"hosts": 0,
|
|
198
|
+
"services": 0,
|
|
199
|
+
"vulnerabilities": 0,
|
|
200
|
+
"credentials": 0,
|
|
201
|
+
"web_paths": 0,
|
|
202
|
+
"notes": 0,
|
|
170
203
|
}
|
|
171
204
|
|
|
172
205
|
try:
|
|
@@ -178,59 +211,67 @@ class SmartImporter:
|
|
|
178
211
|
cm = CredentialsManager()
|
|
179
212
|
|
|
180
213
|
# Import hosts
|
|
181
|
-
if
|
|
182
|
-
hosts = root.findall(
|
|
214
|
+
if "hosts" in import_types:
|
|
215
|
+
hosts = root.findall(".//host")
|
|
183
216
|
for host_elem in hosts:
|
|
184
|
-
address = host_elem.find(
|
|
217
|
+
address = host_elem.find("address")
|
|
185
218
|
if address is not None and address.text:
|
|
186
|
-
host_data = {
|
|
219
|
+
host_data = {"ip": address.text}
|
|
187
220
|
|
|
188
|
-
name = host_elem.find(
|
|
221
|
+
name = host_elem.find("name")
|
|
189
222
|
if name is not None and name.text:
|
|
190
|
-
host_data[
|
|
223
|
+
host_data["hostname"] = name.text
|
|
191
224
|
|
|
192
|
-
os_name = host_elem.find(
|
|
225
|
+
os_name = host_elem.find("os-name")
|
|
193
226
|
if os_name is not None and os_name.text:
|
|
194
|
-
host_data[
|
|
227
|
+
host_data["os"] = os_name.text
|
|
195
228
|
|
|
196
|
-
host_data[
|
|
229
|
+
host_data["status"] = "up"
|
|
197
230
|
host_id = hm.add_or_update_host(engagement_id, host_data)
|
|
198
|
-
results[
|
|
231
|
+
results["hosts"] += 1
|
|
199
232
|
|
|
200
233
|
# Import services for this host
|
|
201
|
-
if
|
|
202
|
-
for svc in host_elem.findall(
|
|
203
|
-
port = svc.find(
|
|
204
|
-
proto = svc.find(
|
|
205
|
-
name = svc.find(
|
|
206
|
-
info = svc.find(
|
|
207
|
-
state = svc.find(
|
|
234
|
+
if "services" in import_types:
|
|
235
|
+
for svc in host_elem.findall(".//service"):
|
|
236
|
+
port = svc.find("port")
|
|
237
|
+
proto = svc.find("proto")
|
|
238
|
+
name = svc.find("name")
|
|
239
|
+
info = svc.find("info")
|
|
240
|
+
state = svc.find("state")
|
|
208
241
|
|
|
209
242
|
if port is not None:
|
|
210
243
|
hm.add_service(
|
|
211
244
|
host_id=host_id,
|
|
212
245
|
port=int(port.text),
|
|
213
|
-
protocol=
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
246
|
+
protocol=(
|
|
247
|
+
proto.text if proto is not None else "tcp"
|
|
248
|
+
),
|
|
249
|
+
state=(
|
|
250
|
+
state.text if state is not None else "open"
|
|
251
|
+
),
|
|
252
|
+
service_name=(
|
|
253
|
+
name.text if name is not None else None
|
|
254
|
+
),
|
|
255
|
+
service_version=(
|
|
256
|
+
info.text if info is not None else None
|
|
257
|
+
),
|
|
217
258
|
)
|
|
218
|
-
results[
|
|
259
|
+
results["services"] += 1
|
|
219
260
|
|
|
220
261
|
# Import credentials
|
|
221
|
-
if
|
|
222
|
-
creds = root.findall(
|
|
262
|
+
if "credentials" in import_types:
|
|
263
|
+
creds = root.findall(".//cred")
|
|
223
264
|
for cred in creds:
|
|
224
|
-
user = cred.find(
|
|
225
|
-
pass_elem = cred.find(
|
|
226
|
-
service_name = cred.find(
|
|
265
|
+
user = cred.find("user")
|
|
266
|
+
pass_elem = cred.find("pass")
|
|
267
|
+
service_name = cred.find("service")
|
|
227
268
|
|
|
228
269
|
if user is not None:
|
|
229
270
|
# Try to find associated host
|
|
230
|
-
host_elem = cred.find(
|
|
271
|
+
host_elem = cred.find(".//host")
|
|
231
272
|
target_host = None
|
|
232
273
|
if host_elem is not None:
|
|
233
|
-
addr = host_elem.find(
|
|
274
|
+
addr = host_elem.find("address")
|
|
234
275
|
if addr is not None:
|
|
235
276
|
target_host = addr.text
|
|
236
277
|
|
|
@@ -239,59 +280,65 @@ class SmartImporter:
|
|
|
239
280
|
username=user.text,
|
|
240
281
|
password=pass_elem.text if pass_elem is not None else None,
|
|
241
282
|
host=target_host,
|
|
242
|
-
service=
|
|
243
|
-
|
|
283
|
+
service=(
|
|
284
|
+
service_name.text if service_name is not None else None
|
|
285
|
+
),
|
|
286
|
+
source="msf_import",
|
|
244
287
|
)
|
|
245
|
-
results[
|
|
288
|
+
results["credentials"] += 1
|
|
246
289
|
|
|
247
290
|
# Import vulnerabilities as findings
|
|
248
|
-
if
|
|
249
|
-
notes = root.findall(
|
|
291
|
+
if "vulnerabilities" in import_types:
|
|
292
|
+
notes = root.findall(".//note")
|
|
250
293
|
for note in notes:
|
|
251
|
-
ntype = note.find(
|
|
252
|
-
data = note.find(
|
|
294
|
+
ntype = note.find("ntype")
|
|
295
|
+
data = note.find("data")
|
|
253
296
|
|
|
254
297
|
if ntype is not None and data is not None:
|
|
255
|
-
if
|
|
298
|
+
if "vuln" in ntype.text.lower() or self._looks_like_vuln(
|
|
299
|
+
data.text
|
|
300
|
+
):
|
|
256
301
|
# Extract host if available
|
|
257
|
-
host_elem = note.find(
|
|
302
|
+
host_elem = note.find(".//host")
|
|
258
303
|
host_ip = None
|
|
259
304
|
host_id = None
|
|
260
305
|
|
|
261
306
|
if host_elem is not None:
|
|
262
|
-
addr = host_elem.find(
|
|
307
|
+
addr = host_elem.find("address")
|
|
263
308
|
if addr is not None:
|
|
264
309
|
host_ip = addr.text
|
|
265
310
|
host_obj = hm.get_host_by_ip(engagement_id, host_ip)
|
|
266
311
|
if host_obj:
|
|
267
|
-
host_id = host_obj[
|
|
312
|
+
host_id = host_obj["id"]
|
|
268
313
|
|
|
269
314
|
fm.add_finding(
|
|
270
315
|
engagement_id=engagement_id,
|
|
271
316
|
host_id=host_id,
|
|
272
317
|
title=ntype.text,
|
|
273
|
-
finding_type=
|
|
274
|
-
severity=
|
|
318
|
+
finding_type="vulnerability",
|
|
319
|
+
severity="medium",
|
|
275
320
|
description=data.text,
|
|
276
|
-
tool=
|
|
321
|
+
tool="msf_import",
|
|
277
322
|
)
|
|
278
|
-
results[
|
|
323
|
+
results["vulnerabilities"] += 1
|
|
279
324
|
|
|
280
325
|
# Import web vulnerabilities
|
|
281
|
-
if
|
|
282
|
-
web_vulns = root.findall(
|
|
326
|
+
if "web_paths" in import_types:
|
|
327
|
+
web_vulns = root.findall(".//web_vuln")
|
|
283
328
|
for wv in web_vulns:
|
|
284
|
-
path = wv.find(
|
|
285
|
-
method = wv.find(
|
|
286
|
-
pname = wv.find(
|
|
287
|
-
proof = wv.find(
|
|
329
|
+
path = wv.find("path")
|
|
330
|
+
method = wv.find("method")
|
|
331
|
+
pname = wv.find("pname")
|
|
332
|
+
proof = wv.find("proof")
|
|
288
333
|
|
|
289
334
|
if path is not None:
|
|
290
335
|
title = f"Web vulnerability in {path.text}"
|
|
291
336
|
if pname is not None:
|
|
292
337
|
title += f" (parameter: {pname.text})"
|
|
293
338
|
|
|
294
|
-
description =
|
|
339
|
+
description = (
|
|
340
|
+
f"Method: {method.text if method is not None else 'N/A'}\n"
|
|
341
|
+
)
|
|
295
342
|
description += f"Path: {path.text}\n"
|
|
296
343
|
if proof is not None:
|
|
297
344
|
description += f"Proof: {proof.text}\n"
|
|
@@ -300,23 +347,23 @@ class SmartImporter:
|
|
|
300
347
|
engagement_id=engagement_id,
|
|
301
348
|
host_id=None,
|
|
302
349
|
title=title,
|
|
303
|
-
finding_type=
|
|
304
|
-
severity=
|
|
350
|
+
finding_type="web_vulnerability",
|
|
351
|
+
severity="medium",
|
|
305
352
|
description=description,
|
|
306
|
-
tool=
|
|
307
|
-
path=path.text if path is not None else None
|
|
353
|
+
tool="msf_import",
|
|
354
|
+
path=path.text if path is not None else None,
|
|
308
355
|
)
|
|
309
|
-
results[
|
|
356
|
+
results["web_paths"] += 1
|
|
310
357
|
|
|
311
358
|
return results
|
|
312
359
|
|
|
313
360
|
except Exception as e:
|
|
314
|
-
return {
|
|
361
|
+
return {"error": str(e)}
|
|
315
362
|
|
|
316
363
|
|
|
317
364
|
def format_preview_summary(analysis: Dict[str, Any]) -> str:
|
|
318
365
|
"""Format analysis results into readable summary."""
|
|
319
|
-
if
|
|
366
|
+
if "error" in analysis:
|
|
320
367
|
return f"Error analyzing file: {analysis['error']}"
|
|
321
368
|
|
|
322
369
|
lines = []
|
|
@@ -325,17 +372,31 @@ def format_preview_summary(analysis: Dict[str, Any]) -> str:
|
|
|
325
372
|
lines.append("=" * 70 + "\n")
|
|
326
373
|
|
|
327
374
|
total_items = sum(
|
|
328
|
-
analysis[key][
|
|
329
|
-
for key in [
|
|
375
|
+
analysis[key]["count"]
|
|
376
|
+
for key in [
|
|
377
|
+
"hosts",
|
|
378
|
+
"services",
|
|
379
|
+
"vulnerabilities",
|
|
380
|
+
"credentials",
|
|
381
|
+
"web_paths",
|
|
382
|
+
"notes",
|
|
383
|
+
]
|
|
330
384
|
if key in analysis
|
|
331
385
|
)
|
|
332
386
|
|
|
333
387
|
lines.append(f"Total items detected: {total_items}\n")
|
|
334
388
|
|
|
335
|
-
for data_type in [
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
389
|
+
for data_type in [
|
|
390
|
+
"hosts",
|
|
391
|
+
"services",
|
|
392
|
+
"vulnerabilities",
|
|
393
|
+
"credentials",
|
|
394
|
+
"web_paths",
|
|
395
|
+
"notes",
|
|
396
|
+
]:
|
|
397
|
+
if data_type in analysis and analysis[data_type]["count"] > 0:
|
|
398
|
+
count = analysis[data_type]["count"]
|
|
399
|
+
preview = analysis[data_type]["preview"]
|
|
339
400
|
|
|
340
401
|
lines.append(f" ✓ {data_type.upper()}: {count}")
|
|
341
402
|
|
|
@@ -35,16 +35,16 @@ from souleyez.integrations.siem.factory import SIEMFactory
|
|
|
35
35
|
|
|
36
36
|
__all__ = [
|
|
37
37
|
# Base classes
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
38
|
+
"SIEMClient",
|
|
39
|
+
"SIEMAlert",
|
|
40
|
+
"SIEMRule",
|
|
41
|
+
"SIEMConnectionStatus",
|
|
42
42
|
# Factory
|
|
43
|
-
|
|
43
|
+
"SIEMFactory",
|
|
44
44
|
# Implementations
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
45
|
+
"WazuhSIEMClient",
|
|
46
|
+
"SplunkSIEMClient",
|
|
47
|
+
"ElasticSIEMClient",
|
|
48
|
+
"SentinelSIEMClient",
|
|
49
|
+
"GoogleSecOpsSIEMClient",
|
|
50
50
|
]
|
|
@@ -35,7 +35,7 @@ class SIEMAlert:
|
|
|
35
35
|
self,
|
|
36
36
|
attack_type: str,
|
|
37
37
|
source_ip: Optional[str] = None,
|
|
38
|
-
dest_ip: Optional[str] = None
|
|
38
|
+
dest_ip: Optional[str] = None,
|
|
39
39
|
) -> bool:
|
|
40
40
|
"""Check if this alert matches an attack pattern.
|
|
41
41
|
|
|
@@ -55,7 +55,7 @@ class SIEMAlert:
|
|
|
55
55
|
|
|
56
56
|
# Attack type is typically checked against rule patterns
|
|
57
57
|
# This is a base implementation - subclasses may override
|
|
58
|
-
attack_keywords = attack_type.lower().split(
|
|
58
|
+
attack_keywords = attack_type.lower().split("_")
|
|
59
59
|
rule_text = f"{self.rule_name} {self.description}".lower()
|
|
60
60
|
|
|
61
61
|
return any(kw in rule_text for kw in attack_keywords)
|
|
@@ -121,7 +121,7 @@ class SIEMClient(ABC):
|
|
|
121
121
|
dest_ip: Optional[str] = None,
|
|
122
122
|
rule_ids: Optional[List[str]] = None,
|
|
123
123
|
search_text: Optional[str] = None,
|
|
124
|
-
limit: int = 100
|
|
124
|
+
limit: int = 100,
|
|
125
125
|
) -> List[SIEMAlert]:
|
|
126
126
|
"""Query alerts from the SIEM.
|
|
127
127
|
|
|
@@ -141,9 +141,7 @@ class SIEMClient(ABC):
|
|
|
141
141
|
|
|
142
142
|
@abstractmethod
|
|
143
143
|
def get_rules(
|
|
144
|
-
self,
|
|
145
|
-
rule_ids: Optional[List[str]] = None,
|
|
146
|
-
enabled_only: bool = True
|
|
144
|
+
self, rule_ids: Optional[List[str]] = None, enabled_only: bool = True
|
|
147
145
|
) -> List[SIEMRule]:
|
|
148
146
|
"""Get detection rules from the SIEM.
|
|
149
147
|
|
|
@@ -179,7 +177,7 @@ class SIEMClient(ABC):
|
|
|
179
177
|
attack_time: datetime,
|
|
180
178
|
source_ip: Optional[str] = None,
|
|
181
179
|
target_ip: Optional[str] = None,
|
|
182
|
-
time_window_minutes: int = 5
|
|
180
|
+
time_window_minutes: int = 5,
|
|
183
181
|
) -> List[SIEMAlert]:
|
|
184
182
|
"""Search for alerts related to a specific attack.
|
|
185
183
|
|
|
@@ -207,12 +205,13 @@ class SIEMClient(ABC):
|
|
|
207
205
|
start_time=start_time,
|
|
208
206
|
end_time=end_time,
|
|
209
207
|
source_ip=source_ip,
|
|
210
|
-
dest_ip=target_ip
|
|
208
|
+
dest_ip=target_ip,
|
|
211
209
|
)
|
|
212
210
|
|
|
213
211
|
# Filter by attack type patterns
|
|
214
212
|
matching_alerts = [
|
|
215
|
-
alert
|
|
213
|
+
alert
|
|
214
|
+
for alert in alerts
|
|
216
215
|
if alert.matches_attack(attack_type, source_ip, target_ip)
|
|
217
216
|
]
|
|
218
217
|
|
|
@@ -224,7 +223,7 @@ class SIEMClient(ABC):
|
|
|
224
223
|
attack_time: datetime,
|
|
225
224
|
source_ip: Optional[str] = None,
|
|
226
225
|
target_ip: Optional[str] = None,
|
|
227
|
-
expected_rule_ids: Optional[List[str]] = None
|
|
226
|
+
expected_rule_ids: Optional[List[str]] = None,
|
|
228
227
|
) -> Dict[str, Any]:
|
|
229
228
|
"""Check if an attack was detected by the SIEM.
|
|
230
229
|
|
|
@@ -247,23 +246,23 @@ class SIEMClient(ABC):
|
|
|
247
246
|
attack_type=attack_type,
|
|
248
247
|
attack_time=attack_time,
|
|
249
248
|
source_ip=source_ip,
|
|
250
|
-
target_ip=target_ip
|
|
249
|
+
target_ip=target_ip,
|
|
251
250
|
)
|
|
252
251
|
|
|
253
252
|
matched_rules = list(set(alert.rule_id for alert in alerts))
|
|
254
253
|
|
|
255
254
|
result = {
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
255
|
+
"detected": len(alerts) > 0,
|
|
256
|
+
"alerts": alerts,
|
|
257
|
+
"alert_count": len(alerts),
|
|
258
|
+
"matched_rules": matched_rules,
|
|
259
|
+
"matched_rule_count": len(matched_rules),
|
|
261
260
|
}
|
|
262
261
|
|
|
263
262
|
if expected_rule_ids:
|
|
264
263
|
expected_set = set(str(r) for r in expected_rule_ids)
|
|
265
264
|
matched_set = set(str(r) for r in matched_rules)
|
|
266
|
-
result[
|
|
267
|
-
result[
|
|
265
|
+
result["expected_rules_matched"] = list(expected_set & matched_set)
|
|
266
|
+
result["expected_rules_missed"] = list(expected_set - matched_set)
|
|
268
267
|
|
|
269
268
|
return result
|