patchi 0.7.5__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.
- patchi/__init__.py +1 -0
- patchi/__main__.py +13 -0
- patchi/cli/__init__.py +0 -0
- patchi/cli/command_families.py +235 -0
- patchi/cli/commands/__init__.py +0 -0
- patchi/cli/commands/access_cmd.py +65 -0
- patchi/cli/commands/agent_stats_cmd.py +273 -0
- patchi/cli/commands/agents_cmd.py +187 -0
- patchi/cli/commands/ai_cmd.py +308 -0
- patchi/cli/commands/assure_cmd.py +538 -0
- patchi/cli/commands/audit_cmd.py +286 -0
- patchi/cli/commands/auto_cmd.py +110 -0
- patchi/cli/commands/blame_cmd.py +53 -0
- patchi/cli/commands/chains_cmd.py +327 -0
- patchi/cli/commands/charter_cmd.py +173 -0
- patchi/cli/commands/chat_cmd.py +597 -0
- patchi/cli/commands/check_cmd.py +232 -0
- patchi/cli/commands/cleanup_cmd.py +328 -0
- patchi/cli/commands/cockpit_cmd.py +208 -0
- patchi/cli/commands/commands_cmd.py +160 -0
- patchi/cli/commands/cross_repo_cmd.py +113 -0
- patchi/cli/commands/deps_cmd.py +110 -0
- patchi/cli/commands/dev_check_cmd.py +342 -0
- patchi/cli/commands/dev_cmd.py +445 -0
- patchi/cli/commands/doctor_cmd.py +457 -0
- patchi/cli/commands/findings_cmd.py +187 -0
- patchi/cli/commands/fix_cmd.py +648 -0
- patchi/cli/commands/fix_review_cmd.py +273 -0
- patchi/cli/commands/heatmap_cmd.py +62 -0
- patchi/cli/commands/help_cmd.py +159 -0
- patchi/cli/commands/hosted_cmd.py +980 -0
- patchi/cli/commands/init.py +456 -0
- patchi/cli/commands/key_cmd.py +571 -0
- patchi/cli/commands/learn_cmd.py +140 -0
- patchi/cli/commands/link_cmd.py +104 -0
- patchi/cli/commands/log_cmd.py +57 -0
- patchi/cli/commands/memory_cmd.py +215 -0
- patchi/cli/commands/model_cmd.py +207 -0
- patchi/cli/commands/notify_cmd.py +213 -0
- patchi/cli/commands/notify_prompts.py +29 -0
- patchi/cli/commands/patch_cmd.py +231 -0
- patchi/cli/commands/plan_cmd.py +128 -0
- patchi/cli/commands/plugins_cmd.py +307 -0
- patchi/cli/commands/queue_cmd.py +152 -0
- patchi/cli/commands/quick_cmd.py +159 -0
- patchi/cli/commands/ready_cmd.py +292 -0
- patchi/cli/commands/report_cmd.py +416 -0
- patchi/cli/commands/restrict_cmd.py +127 -0
- patchi/cli/commands/review_cmd.py +225 -0
- patchi/cli/commands/rules_cmd.py +233 -0
- patchi/cli/commands/scan_cmd.py +1908 -0
- patchi/cli/commands/settings_cmd.py +45 -0
- patchi/cli/commands/status_cmd.py +679 -0
- patchi/cli/commands/test_cmd.py +1052 -0
- patchi/cli/commands/trend_cmd.py +296 -0
- patchi/cli/commands/undo_cmd.py +235 -0
- patchi/cli/commands/update_cmd.py +345 -0
- patchi/cli/commands/verify_cmd.py +56 -0
- patchi/cli/commands/vr_cmd.py +440 -0
- patchi/cli/commands/watch_cmd.py +304 -0
- patchi/cli/commands/web_cmd.py +114 -0
- patchi/cli/console.py +101 -0
- patchi/cli/display/__init__.py +1 -0
- patchi/cli/display/live_audit.py +269 -0
- patchi/cli/display/live_progress.py +169 -0
- patchi/cli/framework.py +241 -0
- patchi/cli/logo.py +128 -0
- patchi/cli/main.py +222 -0
- patchi/cli/registry.py +1171 -0
- patchi/cli/style.py +68 -0
- patchi/cli/themes.py +187 -0
- patchi/cli/ux.py +444 -0
- patchi/core/__init__.py +0 -0
- patchi/core/agents/__init__.py +0 -0
- patchi/core/agents/api_fuzzer_agent.py +141 -0
- patchi/core/agents/attack_agent.py +242 -0
- patchi/core/agents/base.py +1012 -0
- patchi/core/agents/bug_predictor_agent.py +58 -0
- patchi/core/agents/build_tool_validator.py +150 -0
- patchi/core/agents/cache.py +192 -0
- patchi/core/agents/chaos_agent.py +133 -0
- patchi/core/agents/cicd_generator.py +240 -0
- patchi/core/agents/comment_scanner.py +280 -0
- patchi/core/agents/console_logging_agent.py +156 -0
- patchi/core/agents/coordinator.py +858 -0
- patchi/core/agents/core_scanner.py +269 -0
- patchi/core/agents/coverage_prioritizer.py +181 -0
- patchi/core/agents/dead_code_hygiene.py +296 -0
- patchi/core/agents/dead_code_scanner.py +692 -0
- patchi/core/agents/dependency_scanner.py +725 -0
- patchi/core/agents/doc_claim_agent.py +573 -0
- patchi/core/agents/duplicate_scanner.py +568 -0
- patchi/core/agents/env_scanner.py +410 -0
- patchi/core/agents/feature_flag_agent.py +89 -0
- patchi/core/agents/frontend_framework_agent.py +133 -0
- patchi/core/agents/goroutine_agent.py +82 -0
- patchi/core/agents/governor.py +1972 -0
- patchi/core/agents/i18n_agent.py +95 -0
- patchi/core/agents/license_compliance.py +247 -0
- patchi/core/agents/linking_agent.py +251 -0
- patchi/core/agents/memory_profiler_agent.py +102 -0
- patchi/core/agents/modernization_agent.py +84 -0
- patchi/core/agents/mutation_agent.py +174 -0
- patchi/core/agents/nl_query_agent.py +74 -0
- patchi/core/agents/promise_rejection_agent.py +100 -0
- patchi/core/agents/refactoring_agent.py +286 -0
- patchi/core/agents/resource_leak_agent.py +91 -0
- patchi/core/agents/route_graph_scanner.py +951 -0
- patchi/core/agents/run_agent.py +106 -0
- patchi/core/agents/rust_unwrap_agent.py +100 -0
- patchi/core/agents/sbom_generator.py +284 -0
- patchi/core/agents/scanners.py +67 -0
- patchi/core/agents/side/__init__.py +1 -0
- patchi/core/agents/side/build_agent.py +65 -0
- patchi/core/agents/side/format_agent.py +50 -0
- patchi/core/agents/side/install_agent.py +63 -0
- patchi/core/agents/side_file_scanner.py +549 -0
- patchi/core/agents/snapshot_drift_detector.py +146 -0
- patchi/core/agents/spa_route_inventory.py +203 -0
- patchi/core/agents/test_scanner.py +442 -0
- patchi/core/agents/tool_health.py +109 -0
- patchi/core/agents/tool_runner.py +162 -0
- patchi/core/agents/type_scanner.py +168 -0
- patchi/core/agents/ui_scanner.py +558 -0
- patchi/core/ai/__init__.py +20 -0
- patchi/core/ai/agent_audit.py +153 -0
- patchi/core/ai/agent_profiler.py +277 -0
- patchi/core/ai/client.py +499 -0
- patchi/core/ai/cost_tracker.py +107 -0
- patchi/core/ai/model_router.py +318 -0
- patchi/core/ai/orchestrator.py +625 -0
- patchi/core/ai/prompts.py +681 -0
- patchi/core/ai/smart.py +302 -0
- patchi/core/ai/tool_executor.py +24 -0
- patchi/core/ai/tools/__init__.py +10 -0
- patchi/core/ai/tools/executor.py +621 -0
- patchi/core/ai/tools/realize.py +1160 -0
- patchi/core/ai/tools/registry.py +1577 -0
- patchi/core/assurance/__init__.py +33 -0
- patchi/core/assurance/builder.py +211 -0
- patchi/core/assurance/graph.py +342 -0
- patchi/core/assurance/invariants.py +168 -0
- patchi/core/atomic.py +61 -0
- patchi/core/attackers/__init__.py +17 -0
- patchi/core/attackers/api_abuse.py +89 -0
- patchi/core/attackers/auth_bypass.py +73 -0
- patchi/core/attackers/base.py +91 -0
- patchi/core/attackers/business_logic.py +88 -0
- patchi/core/attackers/planner.py +108 -0
- patchi/core/attackers/priv_escalation.py +88 -0
- patchi/core/attackers/recon.py +87 -0
- patchi/core/brain/__init__.py +0 -0
- patchi/core/brain/ast_utils/__init__.py +81 -0
- patchi/core/brain/ast_utils/assignments.py +219 -0
- patchi/core/brain/ast_utils/calls.py +139 -0
- patchi/core/brain/ast_utils/config.py +132 -0
- patchi/core/brain/ast_utils/control_flow.py +74 -0
- patchi/core/brain/ast_utils/dead_symbols.py +205 -0
- patchi/core/brain/ast_utils/decorators.py +108 -0
- patchi/core/brain/ast_utils/helpers.py +144 -0
- patchi/core/brain/ast_utils/imports.py +169 -0
- patchi/core/brain/ast_utils/scan.py +143 -0
- patchi/core/brain/ast_utils/taint.py +310 -0
- patchi/core/brain/audit.py +379 -0
- patchi/core/brain/baseline.py +195 -0
- patchi/core/brain/blast_radius.py +312 -0
- patchi/core/brain/body_tags.py +144 -0
- patchi/core/brain/brain.py +1161 -0
- patchi/core/brain/brain_context.py +393 -0
- patchi/core/brain/brain_watcher.py +340 -0
- patchi/core/brain/charter.py +473 -0
- patchi/core/brain/classifier.py +214 -0
- patchi/core/brain/cockpit.py +350 -0
- patchi/core/brain/code_query.py +710 -0
- patchi/core/brain/contract.py +889 -0
- patchi/core/brain/contract_diff.py +155 -0
- patchi/core/brain/council.py +800 -0
- patchi/core/brain/cross_repo.py +104 -0
- patchi/core/brain/dead_code_tools.py +157 -0
- patchi/core/brain/doc_validator.py +197 -0
- patchi/core/brain/domain_activator.py +2183 -0
- patchi/core/brain/enriched_context.py +220 -0
- patchi/core/brain/file_corpus.py +220 -0
- patchi/core/brain/framework.py +953 -0
- patchi/core/brain/freshness.py +273 -0
- patchi/core/brain/git_aware.py +206 -0
- patchi/core/brain/heatmap.py +104 -0
- patchi/core/brain/ignore_parser.py +153 -0
- patchi/core/brain/import_graph.py +486 -0
- patchi/core/brain/languages.py +397 -0
- patchi/core/brain/layered_brain.py +419 -0
- patchi/core/brain/learning.py +229 -0
- patchi/core/brain/orphaned_endpoints.py +313 -0
- patchi/core/brain/personas/__init__.py +648 -0
- patchi/core/brain/personas/bad_user.py +165 -0
- patchi/core/brain/personas/base.py +349 -0
- patchi/core/brain/proactive.py +807 -0
- patchi/core/brain/project_context.py +121 -0
- patchi/core/brain/project_reader.py +521 -0
- patchi/core/brain/reasoning.py +269 -0
- patchi/core/brain/route_detector/__init__.py +15 -0
- patchi/core/brain/route_detector/base.py +31 -0
- patchi/core/brain/route_detector/csharp.py +209 -0
- patchi/core/brain/route_detector/file_based.py +81 -0
- patchi/core/brain/route_detector/go.py +152 -0
- patchi/core/brain/route_detector/java.py +195 -0
- patchi/core/brain/route_detector/javascript.py +197 -0
- patchi/core/brain/route_detector/php.py +158 -0
- patchi/core/brain/route_detector/python.py +193 -0
- patchi/core/brain/route_detector/registry.py +25 -0
- patchi/core/brain/route_detector/ruby.py +134 -0
- patchi/core/brain/route_detector/rust.py +216 -0
- patchi/core/brain/route_detector/swift.py +118 -0
- patchi/core/brain/route_mapper.py +265 -0
- patchi/core/brain/scanner.py +2136 -0
- patchi/core/brain/secrets.py +280 -0
- patchi/core/brain/symbol_graph.py +1955 -0
- patchi/core/brain/team.py +28 -0
- patchi/core/brain/trace_log.py +132 -0
- patchi/core/brain/type_checker/__init__.py +65 -0
- patchi/core/brain/type_checker/base.py +60 -0
- patchi/core/brain/type_checker/csharp.py +59 -0
- patchi/core/brain/type_checker/dart.py +59 -0
- patchi/core/brain/type_checker/go.py +81 -0
- patchi/core/brain/type_checker/java.py +75 -0
- patchi/core/brain/type_checker/kotlin.py +75 -0
- patchi/core/brain/type_checker/php.py +88 -0
- patchi/core/brain/type_checker/python.py +87 -0
- patchi/core/brain/type_checker/rust.py +81 -0
- patchi/core/brain/type_checker/swift.py +59 -0
- patchi/core/brain/type_checker/typescript.py +298 -0
- patchi/core/brain/understander.py +130 -0
- patchi/core/brain/verify.py +379 -0
- patchi/core/campaigns/__init__.py +14 -0
- patchi/core/campaigns/base.py +62 -0
- patchi/core/campaigns/data_flow_abuse.py +153 -0
- patchi/core/campaigns/orchestrator.py +140 -0
- patchi/core/campaigns/state_transitions.py +178 -0
- patchi/core/ci_bundle.py +104 -0
- patchi/core/config.py +311 -0
- patchi/core/constants.py +245 -0
- patchi/core/debug/__init__.py +25 -0
- patchi/core/debug/adapters/__init__.py +11 -0
- patchi/core/debug/adapters/codelldb.py +379 -0
- patchi/core/debug/adapters/node.py +222 -0
- patchi/core/debug/adapters/powershell.py +210 -0
- patchi/core/debug/adapters/python.py +244 -0
- patchi/core/debug/capture.py +210 -0
- patchi/core/debug/dap_client.py +268 -0
- patchi/core/detector/__init__.py +0 -0
- patchi/core/detector/audit.py +194 -0
- patchi/core/detector/bus.py +226 -0
- patchi/core/detector/dispatcher.py +275 -0
- patchi/core/detector/event.py +254 -0
- patchi/core/detector/sigma_engine.py +378 -0
- patchi/core/detector/triage.py +289 -0
- patchi/core/export/sarif.py +113 -0
- patchi/core/fix/__init__.py +17 -0
- patchi/core/fix/applier.py +574 -0
- patchi/core/fix/base.py +510 -0
- patchi/core/fix/code_fixer.py +169 -0
- patchi/core/fix/dead_code_remover.py +131 -0
- patchi/core/fix/fix_agents.py +775 -0
- patchi/core/fix/patch.py +444 -0
- patchi/core/fix/risk_gate.py +383 -0
- patchi/core/fix/security_fixer.py +93 -0
- patchi/core/fix/verify_loop.py +204 -0
- patchi/core/fuzz/__init__.py +8 -0
- patchi/core/fuzz/corpus.py +137 -0
- patchi/core/fuzz/input_fuzzer.py +256 -0
- patchi/core/fuzz/sequence_fuzzer.py +162 -0
- patchi/core/fuzz/state_fuzzer.py +236 -0
- patchi/core/health.py +407 -0
- patchi/core/hosted/__init__.py +1 -0
- patchi/core/hosted/anomaly.py +298 -0
- patchi/core/hosted/audit_log.py +102 -0
- patchi/core/hosted/compliance_report.py +148 -0
- patchi/core/hosted/ip_reputation.py +169 -0
- patchi/core/hosted/log_parsers.py +251 -0
- patchi/core/hosted/tokens.py +127 -0
- patchi/core/hosted/watchlist.py +164 -0
- patchi/core/hosted/webhooks.py +156 -0
- patchi/core/memory.py +437 -0
- patchi/core/notifications/__init__.py +18 -0
- patchi/core/notifications/channels.py +192 -0
- patchi/core/notifications/digest.py +141 -0
- patchi/core/notifications/escalation.py +158 -0
- patchi/core/notifications/notifier.py +182 -0
- patchi/core/notifications/quiet_hours.py +69 -0
- patchi/core/plugins/__init__.py +48 -0
- patchi/core/plugins/analyzer.py +326 -0
- patchi/core/plugins/analyzers/__init__.py +6 -0
- patchi/core/plugins/analyzers/dead_code_analyzer.py +120 -0
- patchi/core/plugins/analyzers/secret_scanner.py +158 -0
- patchi/core/plugins/analyzers/todo_scanner.py +115 -0
- patchi/core/plugins/registry.py +335 -0
- patchi/core/queue.py +375 -0
- patchi/core/reliability/__init__.py +13 -0
- patchi/core/reliability/chaos.py +163 -0
- patchi/core/reliability/fault_injector.py +172 -0
- patchi/core/reliability/idempotency.py +128 -0
- patchi/core/reliability/recovery.py +147 -0
- patchi/core/runtime/__init__.py +5 -0
- patchi/core/runtime/tracer.py +230 -0
- patchi/core/scan_bus.py +156 -0
- patchi/core/security/__init__.py +1 -0
- patchi/core/security/ai_validator.py +462 -0
- patchi/core/security/app_mapper.py +216 -0
- patchi/core/security/app_profile.py +464 -0
- patchi/core/security/attack_feedback.py +141 -0
- patchi/core/security/attack_scenarios/__init__.py +1 -0
- patchi/core/security/attack_tree.py +176 -0
- patchi/core/security/auth_audit_agent.py +344 -0
- patchi/core/security/authz_agent.py +602 -0
- patchi/core/security/auto_fix_proactive.py +620 -0
- patchi/core/security/auto_fixer.py +660 -0
- patchi/core/security/auto_ticket.py +53 -0
- patchi/core/security/bandit_agent.py +152 -0
- patchi/core/security/blast_radius.py +226 -0
- patchi/core/security/browser_tester.py +333 -0
- patchi/core/security/business_logic_agent.py +263 -0
- patchi/core/security/catch_block_auditor.py +395 -0
- patchi/core/security/cdn_cache_agent.py +357 -0
- patchi/core/security/chain_analyzer.py +321 -0
- patchi/core/security/chain_to_assurance.py +168 -0
- patchi/core/security/charter.py +510 -0
- patchi/core/security/cloud_waf_detector.py +295 -0
- patchi/core/security/codeql_agent.py +233 -0
- patchi/core/security/compliance_agent.py +348 -0
- patchi/core/security/confidence_gate.py +423 -0
- patchi/core/security/container_scanner.py +282 -0
- patchi/core/security/crypto_agent.py +378 -0
- patchi/core/security/cve_monitor.py +262 -0
- patchi/core/security/dast_agent.py +865 -0
- patchi/core/security/defectdojo.py +238 -0
- patchi/core/security/defenders/__init__.py +75 -0
- patchi/core/security/defenders/auth_middleware.py +115 -0
- patchi/core/security/defenders/base.py +97 -0
- patchi/core/security/defenders/block_ip.py +71 -0
- patchi/core/security/defenders/block_ws_origin.py +36 -0
- patchi/core/security/defenders/crypto_fix.py +118 -0
- patchi/core/security/defenders/enforce_rate_limit.py +108 -0
- patchi/core/security/defenders/fix_code.py +93 -0
- patchi/core/security/defenders/invalidate_session.py +40 -0
- patchi/core/security/defenders/patch_config.py +70 -0
- patchi/core/security/defenders/rotate_secret.py +54 -0
- patchi/core/security/defenders/suspend_account.py +41 -0
- patchi/core/security/defenders/update_dependency.py +160 -0
- patchi/core/security/defense_layer.py +217 -0
- patchi/core/security/dependency_vulnerability_agent.py +616 -0
- patchi/core/security/detection_pipeline.py +289 -0
- patchi/core/security/dns_security_agent.py +497 -0
- patchi/core/security/domain_activator_v2.py +913 -0
- patchi/core/security/domain_loader.py +1004 -0
- patchi/core/security/domains/__init__.py +0 -0
- patchi/core/security/email_authentication_agent.py +417 -0
- patchi/core/security/env_var_validator.py +304 -0
- patchi/core/security/evidence.py +100 -0
- patchi/core/security/falco_runtime_agent.py +387 -0
- patchi/core/security/fix-playbooks/__init__.py +0 -0
- patchi/core/security/gated_finding.py +85 -0
- patchi/core/security/git_diff_activator.py +367 -0
- patchi/core/security/governance.py +208 -0
- patchi/core/security/gradual_gate.py +34 -0
- patchi/core/security/history.py +315 -0
- patchi/core/security/iac_scanner.py +250 -0
- patchi/core/security/ignore_expiry.py +111 -0
- patchi/core/security/ignore_learner.py +416 -0
- patchi/core/security/injection_agent.py +319 -0
- patchi/core/security/insecure_randomness_agent.py +288 -0
- patchi/core/security/intent_analyzer.py +326 -0
- patchi/core/security/jwt_agent.py +453 -0
- patchi/core/security/kubernetes_agent.py +638 -0
- patchi/core/security/layer2_orchestrator.py +341 -0
- patchi/core/security/llm_security_agent.py +252 -0
- patchi/core/security/misconfig_agent.py +600 -0
- patchi/core/security/mobile_security_agent.py +474 -0
- patchi/core/security/network_agent.py +376 -0
- patchi/core/security/noise_filter.py +274 -0
- patchi/core/security/orchestrator.py +313 -0
- patchi/core/security/pattern_context.py +125 -0
- patchi/core/security/pattern_loader.py +101 -0
- patchi/core/security/pentest/__init__.py +4 -0
- patchi/core/security/pentest/base.py +58 -0
- patchi/core/security/pentest/dalfox_adapter.py +40 -0
- patchi/core/security/pentest/ffuf_adapter.py +48 -0
- patchi/core/security/pentest/nuclei_adapter.py +50 -0
- patchi/core/security/pentest/registry.py +80 -0
- patchi/core/security/pentest/shannon_adapter.py +107 -0
- patchi/core/security/pentest/sqlmap_adapter.py +56 -0
- patchi/core/security/pentest/zap_adapter.py +52 -0
- patchi/core/security/plan_auditor.py +276 -0
- patchi/core/security/policy_engine.py +272 -0
- patchi/core/security/prechecks.py +172 -0
- patchi/core/security/precommit_hook.py +247 -0
- patchi/core/security/privacy_agent.py +361 -0
- patchi/core/security/push_notification_agent.py +417 -0
- patchi/core/security/pysa_agent.py +143 -0
- patchi/core/security/reasoning.py +442 -0
- patchi/core/security/red_team_agent.py +184 -0
- patchi/core/security/red_team_engine.py +1142 -0
- patchi/core/security/remediation.py +360 -0
- patchi/core/security/request_interceptor.py +294 -0
- patchi/core/security/runtime_validator.py +245 -0
- patchi/core/security/saml_sso_agent.py +401 -0
- patchi/core/security/sast_agent.py +213 -0
- patchi/core/security/sast_gate.py +95 -0
- patchi/core/security/scheduler.py +287 -0
- patchi/core/security/secrets_runtime_agent.py +476 -0
- patchi/core/security/security_agents.py +175 -0
- patchi/core/security/security_config.py +551 -0
- patchi/core/security/security_probe.py +762 -0
- patchi/core/security/security_taint.py +1005 -0
- patchi/core/security/sensitive_data_agent.py +271 -0
- patchi/core/security/service_mesh_agent.py +412 -0
- patchi/core/security/session_management_agent.py +245 -0
- patchi/core/security/ssrf_agent.py +422 -0
- patchi/core/security/supply_chain.py +449 -0
- patchi/core/security/threat_model_generator.py +378 -0
- patchi/core/security/threat_model_updater.py +196 -0
- patchi/core/security/tool_adapters.py +135 -0
- patchi/core/security/tool_verify.py +173 -0
- patchi/core/security/websocket_security_agent.py +188 -0
- patchi/core/snapshot.py +242 -0
- patchi/core/tenant.py +386 -0
- patchi/core/testing/__init__.py +17 -0
- patchi/core/testing/_browser.py +147 -0
- patchi/core/testing/ads_agent.py +141 -0
- patchi/core/testing/api_contract_agent.py +450 -0
- patchi/core/testing/app_discovery_agent.py +641 -0
- patchi/core/testing/app_launcher.py +184 -0
- patchi/core/testing/browser_test_agent.py +516 -0
- patchi/core/testing/e2e_flow_agent.py +354 -0
- patchi/core/testing/flake_detector_agent.py +292 -0
- patchi/core/testing/gate.py +31 -0
- patchi/core/testing/live_test_runner.py +304 -0
- patchi/core/testing/live_v2/__init__.py +1 -0
- patchi/core/testing/live_v2/browser_pool.py +349 -0
- patchi/core/testing/live_v2/browser_test_runner.py +313 -0
- patchi/core/testing/live_v2/runner.py +626 -0
- patchi/core/testing/live_v2/screenshot_manager.py +474 -0
- patchi/core/testing/live_v2/stress_orchestrator.py +523 -0
- patchi/core/testing/live_v2/video_recorder.py +303 -0
- patchi/core/testing/regression_agent.py +302 -0
- patchi/core/testing/security_test_agent.py +253 -0
- patchi/core/testing/stress_test_agent.py +203 -0
- patchi/core/testing/test_agents.py +50 -0
- patchi/core/testing/test_config.py +194 -0
- patchi/core/testing/ui_accessibility_agent.py +324 -0
- patchi/core/testing/ui_button_agent.py +294 -0
- patchi/core/testing/ui_layout_agent.py +330 -0
- patchi/core/testing/unit_test_agent.py +1149 -0
- patchi/core/testing/visual_regression_agent.py +260 -0
- patchi/web/__init__.py +1 -0
- patchi/web/api/__init__.py +1 -0
- patchi/web/api/brain_map.py +114 -0
- patchi/web/api/charts.py +47 -0
- patchi/web/api/chat.py +224 -0
- patchi/web/api/cicd.py +576 -0
- patchi/web/api/dast_evidence.py +49 -0
- patchi/web/api/dev_check.py +320 -0
- patchi/web/api/fix.py +134 -0
- patchi/web/api/guard.py +65 -0
- patchi/web/api/hosted.py +128 -0
- patchi/web/api/hosted_v2.py +234 -0
- patchi/web/api/live_testing.py +699 -0
- patchi/web/api/scan.py +582 -0
- patchi/web/api/smart.py +111 -0
- patchi/web/api/tenant.py +117 -0
- patchi/web/api/tools.py +85 -0
- patchi/web/api_legacy.py +2401 -0
- patchi/web/app.py +310 -0
- patchi/web/events.py +483 -0
- patchi/web/routes/__init__.py +1 -0
- patchi/web/routes/assurance.py +541 -0
- patchi/web/routes/brain.py +36 -0
- patchi/web/routes/charter.py +227 -0
- patchi/web/routes/chat.py +23 -0
- patchi/web/routes/dashboard.py +558 -0
- patchi/web/routes/dashboard_v2.py +402 -0
- patchi/web/routes/findings.py +330 -0
- patchi/web/routes/guard.py +35 -0
- patchi/web/routes/history.py +126 -0
- patchi/web/routes/landing.py +52 -0
- patchi/web/routes/live_testing.py +24 -0
- patchi/web/routes/review.py +57 -0
- patchi/web/routes/self_improvement.py +110 -0
- patchi/web/routes/settings.py +49 -0
- patchi/web/routes/tokens.py +29 -0
- patchi/web/spawn.py +139 -0
- patchi/web/ws.py +141 -0
- patchi-0.7.5.dist-info/METADATA +315 -0
- patchi-0.7.5.dist-info/RECORD +496 -0
- patchi-0.7.5.dist-info/WHEEL +5 -0
- patchi-0.7.5.dist-info/licenses/LICENSE +60 -0
- patchi-0.7.5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Runtime Security Validation (Layer 5) — DAST for web apps.
|
|
3
|
+
|
|
4
|
+
Deploys the generated app in an isolated environment and runs:
|
|
5
|
+
- Security header validation on actual HTTP responses
|
|
6
|
+
- TLS/SSL configuration checks
|
|
7
|
+
- HTTP method tampering tests
|
|
8
|
+
- Server information leakage detection
|
|
9
|
+
- Basic active probing (when dev mode enabled)
|
|
10
|
+
|
|
11
|
+
Requires the app to be running (or spins up via Docker if available).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
import subprocess
|
|
18
|
+
import urllib.error
|
|
19
|
+
import urllib.request
|
|
20
|
+
|
|
21
|
+
from patchi.core.agents.base import (
|
|
22
|
+
AgentGroup,
|
|
23
|
+
AgentInput,
|
|
24
|
+
AgentResult,
|
|
25
|
+
BaseAgent,
|
|
26
|
+
Finding,
|
|
27
|
+
Severity,
|
|
28
|
+
register,
|
|
29
|
+
)
|
|
30
|
+
from patchi.core.constants import is_offline
|
|
31
|
+
|
|
32
|
+
# ── Expected security headers ─────────────────────────────────────────────────
|
|
33
|
+
|
|
34
|
+
_REQUIRED_HEADERS = {
|
|
35
|
+
"strict-transport-security": ("HSTS missing", Severity.HIGH, "CWE-319"),
|
|
36
|
+
"x-content-type-options": ("X-Content-Type-Options missing", Severity.MEDIUM, "CWE-693"),
|
|
37
|
+
"x-frame-options": ("X-Frame-Options missing (clickjacking)", Severity.MEDIUM, "CWE-1021"),
|
|
38
|
+
"x-xss-protection": ("X-XSS-Protection missing", Severity.LOW, "CWE-79"),
|
|
39
|
+
"content-security-policy": ("Content-Security-Policy missing", Severity.HIGH, "CWE-693"),
|
|
40
|
+
"referrer-policy": ("Referrer-Policy missing", Severity.LOW, "CWE-200"),
|
|
41
|
+
"permissions-policy": ("Permissions-Policy missing", Severity.LOW, "CWE-250"),
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
_DANGEROUS_HEADERS = {
|
|
45
|
+
"server": ("Server header leaks version info", Severity.LOW, "CWE-200"),
|
|
46
|
+
"x-powered-by": ("X-Powered-By header leaks technology stack", Severity.LOW, "CWE-200"),
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
_SENSITIVE_METHODS = ["PUT", "DELETE", "PATCH", "TRACE", "OPTIONS"]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
_log = logging.getLogger("patchi.security.runtime_validator")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _check_headers(url: str) -> list[dict]:
|
|
56
|
+
"""Fetch URL and check security headers."""
|
|
57
|
+
findings = []
|
|
58
|
+
try:
|
|
59
|
+
req = urllib.request.Request(url, method="GET")
|
|
60
|
+
req.add_header("User-Agent", "Patchi-SecurityValidator/1.0")
|
|
61
|
+
with urllib.request.urlopen(req, timeout=10) as resp:
|
|
62
|
+
headers = {k.lower(): v for k, v in resp.headers.items()}
|
|
63
|
+
|
|
64
|
+
for header, (msg, sev, cwe) in _REQUIRED_HEADERS.items():
|
|
65
|
+
if header not in headers:
|
|
66
|
+
findings.append(
|
|
67
|
+
{"message": msg, "severity": sev.value, "cwe": cwe, "header": header}
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
for header, (msg, sev, cwe) in _DANGEROUS_HEADERS.items():
|
|
71
|
+
if header in headers:
|
|
72
|
+
findings.append(
|
|
73
|
+
{
|
|
74
|
+
"message": f"{msg}: {headers[header]}",
|
|
75
|
+
"severity": sev.value,
|
|
76
|
+
"cwe": cwe,
|
|
77
|
+
"header": header,
|
|
78
|
+
}
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
except (urllib.error.URLError, urllib.error.HTTPError, OSError, ValueError):
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
return findings
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _check_method_tampering(base_url: str) -> list[dict]:
|
|
88
|
+
"""Test if sensitive HTTP methods are allowed."""
|
|
89
|
+
findings = []
|
|
90
|
+
for method in _SENSITIVE_METHODS:
|
|
91
|
+
try:
|
|
92
|
+
req = urllib.request.Request(base_url, method=method)
|
|
93
|
+
req.add_header("User-Agent", "Patchi-SecurityValidator/1.0")
|
|
94
|
+
with urllib.request.urlopen(req, timeout=5) as resp:
|
|
95
|
+
if resp.status < 400:
|
|
96
|
+
findings.append(
|
|
97
|
+
{
|
|
98
|
+
"message": f"HTTP {method} allowed on {base_url} (status {resp.status})",
|
|
99
|
+
"severity": "medium" if method in ("PUT", "DELETE", "PATCH") else "low",
|
|
100
|
+
"cwe": "CWE-749",
|
|
101
|
+
}
|
|
102
|
+
)
|
|
103
|
+
except (urllib.error.HTTPError, urllib.error.URLError, OSError):
|
|
104
|
+
# 405 Method Not Allowed is expected
|
|
105
|
+
pass
|
|
106
|
+
except Exception as e:
|
|
107
|
+
_log.warning("_check_method_tampering failed: %s", e)
|
|
108
|
+
|
|
109
|
+
return findings
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _check_tls(url: str) -> list[dict]:
|
|
113
|
+
"""Basic TLS check — verify HTTPS and cert validity."""
|
|
114
|
+
findings = []
|
|
115
|
+
if url.startswith("http://"):
|
|
116
|
+
findings.append(
|
|
117
|
+
{
|
|
118
|
+
"message": "Application uses plain HTTP — no TLS",
|
|
119
|
+
"severity": "high",
|
|
120
|
+
"cwe": "CWE-319",
|
|
121
|
+
}
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Try sslyze if available
|
|
125
|
+
try:
|
|
126
|
+
from urllib.parse import urlparse
|
|
127
|
+
|
|
128
|
+
parsed = urlparse(url)
|
|
129
|
+
hostname = parsed.hostname or ""
|
|
130
|
+
if hostname:
|
|
131
|
+
proc = subprocess.run(
|
|
132
|
+
["sslyze", "--json", hostname],
|
|
133
|
+
capture_output=True,
|
|
134
|
+
text=True,
|
|
135
|
+
timeout=15,
|
|
136
|
+
)
|
|
137
|
+
if proc.returncode == 0 and proc.stdout.strip():
|
|
138
|
+
import json
|
|
139
|
+
|
|
140
|
+
data = json.loads(proc.stdout)
|
|
141
|
+
for scan in data.get("scan_result", {}).get("tls_cipher_suites", []):
|
|
142
|
+
for suite in scan.get("tls_cipher_suites", []):
|
|
143
|
+
name = suite.get("name", "")
|
|
144
|
+
if any(
|
|
145
|
+
weak in name.upper() for weak in ("RC4", "DES", "NULL", "EXPORT", "MD5")
|
|
146
|
+
):
|
|
147
|
+
findings.append(
|
|
148
|
+
{
|
|
149
|
+
"message": f"Weak TLS cipher: {name}",
|
|
150
|
+
"severity": "high",
|
|
151
|
+
"cwe": "CWE-326",
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
except (FileNotFoundError, subprocess.TimeoutExpired, Exception):
|
|
155
|
+
pass
|
|
156
|
+
|
|
157
|
+
return findings
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# ── Runtime Validator Agent ───────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@register
|
|
164
|
+
class RuntimeValidatorAgent(BaseAgent):
|
|
165
|
+
"""Runtime security validation: headers, TLS, method tampering."""
|
|
166
|
+
|
|
167
|
+
name = "RuntimeValidatorAgent"
|
|
168
|
+
group = AgentGroup.SECURITY
|
|
169
|
+
timeout = 60
|
|
170
|
+
|
|
171
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
172
|
+
if is_offline():
|
|
173
|
+
result.add_finding(
|
|
174
|
+
Finding(
|
|
175
|
+
agent=self.name,
|
|
176
|
+
type="skipped_offline",
|
|
177
|
+
severity=Severity.INFO,
|
|
178
|
+
file="",
|
|
179
|
+
message="Runtime validation skipped — offline mode (no live probing)",
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
return
|
|
183
|
+
|
|
184
|
+
# Look for configured URL in brain or config
|
|
185
|
+
app_url = (
|
|
186
|
+
inp.brain.get("app_url", "")
|
|
187
|
+
or inp.config.get("app_url", "")
|
|
188
|
+
or inp.extra.get("app_url", "")
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
if not app_url:
|
|
192
|
+
result.add_finding(
|
|
193
|
+
Finding(
|
|
194
|
+
agent=self.name,
|
|
195
|
+
type="no_target_url",
|
|
196
|
+
severity=Severity.INFO,
|
|
197
|
+
file="",
|
|
198
|
+
message="No app URL configured — set 'app_url' in config or brain to enable runtime validation",
|
|
199
|
+
)
|
|
200
|
+
)
|
|
201
|
+
return
|
|
202
|
+
|
|
203
|
+
# 1. Header checks
|
|
204
|
+
header_findings = _check_headers(app_url)
|
|
205
|
+
for f in header_findings:
|
|
206
|
+
result.add_finding(
|
|
207
|
+
Finding(
|
|
208
|
+
agent=self.name,
|
|
209
|
+
type="missing_header" if "missing" in f["message"] else "header_leak",
|
|
210
|
+
severity=Severity(f["severity"]),
|
|
211
|
+
file=app_url,
|
|
212
|
+
message=f["message"],
|
|
213
|
+
cwe=f["cwe"],
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# 2. TLS check
|
|
218
|
+
tls_findings = _check_tls(app_url)
|
|
219
|
+
for f in tls_findings:
|
|
220
|
+
result.add_finding(
|
|
221
|
+
Finding(
|
|
222
|
+
agent=self.name,
|
|
223
|
+
type="tls_issue",
|
|
224
|
+
severity=Severity(f["severity"]),
|
|
225
|
+
file=app_url,
|
|
226
|
+
message=f["message"],
|
|
227
|
+
cwe=f["cwe"],
|
|
228
|
+
)
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
# 3. Method tampering
|
|
232
|
+
method_findings = _check_method_tampering(app_url)
|
|
233
|
+
for f in method_findings:
|
|
234
|
+
result.add_finding(
|
|
235
|
+
Finding(
|
|
236
|
+
agent=self.name,
|
|
237
|
+
type="method_tampering",
|
|
238
|
+
severity=Severity(f["severity"]),
|
|
239
|
+
file=app_url,
|
|
240
|
+
message=f["message"],
|
|
241
|
+
cwe=f["cwe"],
|
|
242
|
+
)
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
result.files_scanned = 1
|
|
@@ -0,0 +1,401 @@
|
|
|
1
|
+
"""SAML/SSO security agent — validates SAML implementations against known attack patterns."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import re
|
|
7
|
+
import shutil
|
|
8
|
+
import subprocess
|
|
9
|
+
|
|
10
|
+
from ..agents.base import (
|
|
11
|
+
AgentDomain,
|
|
12
|
+
AgentGroup,
|
|
13
|
+
AgentInput,
|
|
14
|
+
AgentResult,
|
|
15
|
+
AgentStatus,
|
|
16
|
+
BaseAgent,
|
|
17
|
+
Finding,
|
|
18
|
+
Severity,
|
|
19
|
+
make_finding,
|
|
20
|
+
register,
|
|
21
|
+
safe_rglob,
|
|
22
|
+
)
|
|
23
|
+
from ..brain.trace_log import trace_agent
|
|
24
|
+
|
|
25
|
+
# SAML library detection patterns
|
|
26
|
+
SAML_LIBRARIES = {
|
|
27
|
+
"pysaml2": {
|
|
28
|
+
"import": r"from\s+saml2\b|import\s+saml2\b",
|
|
29
|
+
"config": r"saml2\.config|SPConfig|IdPConfig",
|
|
30
|
+
},
|
|
31
|
+
"onelogin": {
|
|
32
|
+
"import": r"from\s+onelogin\.saml2\b|import\s+onelogin\.saml2\b",
|
|
33
|
+
"auth": r"OneLogin_Saml2_Auth|AuthnRequest",
|
|
34
|
+
},
|
|
35
|
+
"python3-saml": {
|
|
36
|
+
"import": r"from\s+onelogin\.saml2\b|import\s+onelogin\.saml2\b",
|
|
37
|
+
"settings": r"saml_settings|SAML_SETTINGS",
|
|
38
|
+
},
|
|
39
|
+
"lxml-saml": {
|
|
40
|
+
"import": r"from\s+lxml\.etree\b|import\s+lxml\.etree\b",
|
|
41
|
+
"etree": r"etree\.fromstring|etree\.parse|etree\.XML",
|
|
42
|
+
},
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
# Dangerous XML patterns (XXE, XSW)
|
|
46
|
+
XXE_PATTERNS = [
|
|
47
|
+
(r"resolve_entities\s*=\s*True", "XML entity resolution enabled"),
|
|
48
|
+
(r"no_network\s*=\s*False", "Network access during XML parsing"),
|
|
49
|
+
(r"DTDLOAD|dtd_load|load_dtd", "DTD loading enabled"),
|
|
50
|
+
(r"ENTITY|entity|<!ENTITY", "XML entity declaration (potential XXE)"),
|
|
51
|
+
(r"SYSTEM\s+[\"']", "XML SYSTEM entity reference"),
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
XSW_PATTERNS = [
|
|
55
|
+
(r"cloneNode|importNode|appendChild.*Signature", "XML Signature Wrapping — node cloning"),
|
|
56
|
+
(r"insertBefore.*Signature", "XML Signature Wrapping — insert before signature"),
|
|
57
|
+
(r"getElementById.*Signature", "XML Signature Wrapping — ID reference"),
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
SIGNATURE_VALIDATION_PATTERNS = [
|
|
61
|
+
(
|
|
62
|
+
r"check_signature|validate_signature|verify_signature|isValidSignature",
|
|
63
|
+
"Signature validation present",
|
|
64
|
+
),
|
|
65
|
+
(r"assertion.*valid|validate.*assertion", "Assertion validation"),
|
|
66
|
+
(r"SignedInfo|Reference.*URI.*DigestValue", "XML signature structure detected"),
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
ASSERTION_LIFETIME_PATTERNS = [
|
|
70
|
+
(r"NotBefore|NotOnOrAfter", "Assertion time bounds present"),
|
|
71
|
+
(r"SessionIndex|session_not_on_or_after", "Session index tracking"),
|
|
72
|
+
(r"clock_slop|clockSkew|ALLOWED_CLOCK_SKEW", "Clock skew tolerance configured"),
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
RELAYSTATE_PATTERNS = [
|
|
76
|
+
(r"RelayState|relay_state", "RelayState handling detected"),
|
|
77
|
+
(r"redirect.*url|return.*url|target.*url", "Redirect URL handling"),
|
|
78
|
+
]
|
|
79
|
+
|
|
80
|
+
CONTROL_IDS = {
|
|
81
|
+
"SAML-01": ("SAML XXE in XML parsing", Severity.CRITICAL),
|
|
82
|
+
"SAML-02": ("XML Signature Wrapping attack vector", Severity.CRITICAL),
|
|
83
|
+
"SAML-03": ("Weak algorithm enforcement (SHA-1)", Severity.HIGH),
|
|
84
|
+
"SAML-04": ("Missing signature validation", Severity.HIGH),
|
|
85
|
+
"SAML-05": ("No assertion lifetime enforcement", Severity.HIGH),
|
|
86
|
+
"SAML-06": ("RelayState open redirect", Severity.MEDIUM),
|
|
87
|
+
"SAML-07": ("SAML library in use", Severity.INFO),
|
|
88
|
+
"SAML-08": ("Assertion replay protection missing", Severity.HIGH),
|
|
89
|
+
"SAML-09": ("Certificate trust not pinned", Severity.MEDIUM),
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
_log = logging.getLogger("patchi.security.saml_sso_agent")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@register
|
|
97
|
+
class SamlSSOAgent(BaseAgent):
|
|
98
|
+
name = "SamlSSOAgent"
|
|
99
|
+
group = AgentGroup.SECURITY
|
|
100
|
+
domain = AgentDomain.SECURITY
|
|
101
|
+
description = (
|
|
102
|
+
"SAML/SSO security: XSW, XXE, algorithm enforcement, assertion replay, certificate trust"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
106
|
+
with trace_agent(self.name, inp.root) as trace:
|
|
107
|
+
findings: list[Finding] = []
|
|
108
|
+
files_scanned = 0
|
|
109
|
+
|
|
110
|
+
# Phase 1: Static analysis of source code
|
|
111
|
+
for fpath in safe_rglob(inp.root, "*.py"):
|
|
112
|
+
files_scanned += 1
|
|
113
|
+
try:
|
|
114
|
+
content = fpath.read_text(encoding="utf-8", errors="replace")
|
|
115
|
+
except Exception as e:
|
|
116
|
+
_log.warning("SamlSSOAgent._run failed: %s", e)
|
|
117
|
+
continue
|
|
118
|
+
|
|
119
|
+
rel = str(fpath.relative_to(inp.root))
|
|
120
|
+
|
|
121
|
+
# Detect SAML library usage
|
|
122
|
+
for lib_name, lib_patterns in SAML_LIBRARIES.items():
|
|
123
|
+
for pattern_key, pattern in lib_patterns.items():
|
|
124
|
+
for m in re.finditer(pattern, content, re.IGNORECASE):
|
|
125
|
+
line_no = content[: m.start()].count("\n") + 1
|
|
126
|
+
findings.append(
|
|
127
|
+
make_finding(
|
|
128
|
+
Severity.INFO,
|
|
129
|
+
rel,
|
|
130
|
+
line_no,
|
|
131
|
+
f"SAML library detected: {lib_name}",
|
|
132
|
+
f"Code uses {lib_name} library ({pattern_key} pattern). Verify SAML security configuration.",
|
|
133
|
+
code_snippet=m.group(),
|
|
134
|
+
suggestion=f"Review {lib_name} configuration for security best practices",
|
|
135
|
+
control_id="SAML-07",
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
# Check for XXE patterns
|
|
140
|
+
for pattern, desc in XXE_PATTERNS:
|
|
141
|
+
for m in re.finditer(pattern, content, re.IGNORECASE):
|
|
142
|
+
line_no = content[: m.start()].count("\n") + 1
|
|
143
|
+
findings.append(
|
|
144
|
+
make_finding(
|
|
145
|
+
Severity.CRITICAL,
|
|
146
|
+
rel,
|
|
147
|
+
line_no,
|
|
148
|
+
"SAML-01: XML External Entity (XXE) vulnerability",
|
|
149
|
+
desc,
|
|
150
|
+
code_snippet=m.group(),
|
|
151
|
+
suggestion="Disable DTD loading and entity resolution in XML parser",
|
|
152
|
+
cwe="CWE-611",
|
|
153
|
+
control_id="SAML-01",
|
|
154
|
+
)
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
# Check for XSW patterns
|
|
158
|
+
for pattern, desc in XSW_PATTERNS:
|
|
159
|
+
for m in re.finditer(pattern, content, re.IGNORECASE):
|
|
160
|
+
line_no = content[: m.start()].count("\n") + 1
|
|
161
|
+
findings.append(
|
|
162
|
+
make_finding(
|
|
163
|
+
Severity.CRITICAL,
|
|
164
|
+
rel,
|
|
165
|
+
line_no,
|
|
166
|
+
"SAML-02: XML Signature Wrapping attack vector",
|
|
167
|
+
desc,
|
|
168
|
+
code_snippet=m.group(),
|
|
169
|
+
suggestion="Use a SAML library that prevents Signature Wrapping attacks",
|
|
170
|
+
cwe="CWE-327",
|
|
171
|
+
control_id="SAML-02",
|
|
172
|
+
)
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
# Check for weak algorithms (SHA-1)
|
|
176
|
+
weak_algo = re.findall(r"sha-?1|SHA-?1|md5|MD5", content)
|
|
177
|
+
if weak_algo:
|
|
178
|
+
for m in re.finditer(r"sha-?1|SHA-?1|md5|MD5", content):
|
|
179
|
+
line_no = content[: m.start()].count("\n") + 1
|
|
180
|
+
findings.append(
|
|
181
|
+
make_finding(
|
|
182
|
+
Severity.HIGH,
|
|
183
|
+
rel,
|
|
184
|
+
line_no,
|
|
185
|
+
"SAML-03: Weak cryptographic algorithm (SHA-1/MD5)",
|
|
186
|
+
"SAML assertion uses deprecated hash algorithm. SHA-1 is vulnerable to collision attacks.",
|
|
187
|
+
code_snippet=m.group(),
|
|
188
|
+
suggestion="Upgrade to SHA-256 or stronger algorithms",
|
|
189
|
+
cwe="CWE-327",
|
|
190
|
+
control_id="SAML-03",
|
|
191
|
+
)
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
# Check for missing signature validation
|
|
195
|
+
has_sig_validation = any(
|
|
196
|
+
re.search(p, content, re.IGNORECASE) for p, _ in SIGNATURE_VALIDATION_PATTERNS
|
|
197
|
+
)
|
|
198
|
+
if (
|
|
199
|
+
re.search(
|
|
200
|
+
r"saml2?\b|onelogin|AuthnRequest|SAMLResponse", content, re.IGNORECASE
|
|
201
|
+
)
|
|
202
|
+
and not has_sig_validation
|
|
203
|
+
):
|
|
204
|
+
findings.append(
|
|
205
|
+
make_finding(
|
|
206
|
+
Severity.HIGH,
|
|
207
|
+
rel,
|
|
208
|
+
0,
|
|
209
|
+
"SAML-04: Missing signature validation",
|
|
210
|
+
"SAML processing code detected without signature validation. Assertions may be forged.",
|
|
211
|
+
suggestion="Implement XML signature validation before processing assertions",
|
|
212
|
+
cwe="CWE-347",
|
|
213
|
+
control_id="SAML-04",
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
# Check for assertion lifetime
|
|
218
|
+
has_lifetime = any(
|
|
219
|
+
re.search(p, content, re.IGNORECASE) for p, _ in ASSERTION_LIFETIME_PATTERNS
|
|
220
|
+
)
|
|
221
|
+
if (
|
|
222
|
+
re.search(r"saml2?\b|assertion|SAMLResponse", content, re.IGNORECASE)
|
|
223
|
+
and not has_lifetime
|
|
224
|
+
):
|
|
225
|
+
findings.append(
|
|
226
|
+
make_finding(
|
|
227
|
+
Severity.HIGH,
|
|
228
|
+
rel,
|
|
229
|
+
0,
|
|
230
|
+
"SAML-05: No assertion lifetime enforcement",
|
|
231
|
+
"SAML assertions processed without checking NotBefore/NotOnOrAfter. Replay attacks possible.",
|
|
232
|
+
suggestion="Enforce assertion time bounds and session index tracking",
|
|
233
|
+
cwe="CWE-294",
|
|
234
|
+
control_id="SAML-05",
|
|
235
|
+
)
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
# Check for RelayState validation
|
|
239
|
+
has_relay = any(
|
|
240
|
+
re.search(p, content, re.IGNORECASE) for p, _ in RELAYSTATE_PATTERNS
|
|
241
|
+
)
|
|
242
|
+
if has_relay:
|
|
243
|
+
# Check if validation is present
|
|
244
|
+
if not re.search(
|
|
245
|
+
r"whitelist|allowlist|validate.*relay|check.*relay|sanitize.*relay",
|
|
246
|
+
content,
|
|
247
|
+
re.IGNORECASE,
|
|
248
|
+
):
|
|
249
|
+
findings.append(
|
|
250
|
+
make_finding(
|
|
251
|
+
Severity.MEDIUM,
|
|
252
|
+
rel,
|
|
253
|
+
0,
|
|
254
|
+
"SAML-06: RelayState open redirect potential",
|
|
255
|
+
"RelayState parameter used without explicit validation. May allow open redirect attacks.",
|
|
256
|
+
suggestion="Validate RelayState against an allowlist of permitted redirect URLs",
|
|
257
|
+
cwe="CWE-601",
|
|
258
|
+
control_id="SAML-06",
|
|
259
|
+
)
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
# Check for assertion replay protection
|
|
263
|
+
if re.search(r"saml2?\b|SAMLResponse", content, re.IGNORECASE):
|
|
264
|
+
if not re.search(
|
|
265
|
+
r"SessionIndex|session_index|replay|nonce|jti", content, re.IGNORECASE
|
|
266
|
+
):
|
|
267
|
+
findings.append(
|
|
268
|
+
make_finding(
|
|
269
|
+
Severity.HIGH,
|
|
270
|
+
rel,
|
|
271
|
+
0,
|
|
272
|
+
"SAML-08: Assertion replay protection missing",
|
|
273
|
+
"No session index or nonce tracking detected. SAML assertions may be replayed.",
|
|
274
|
+
suggestion="Track assertion IDs and session indices to prevent replay attacks",
|
|
275
|
+
cwe="CWE-294",
|
|
276
|
+
control_id="SAML-08",
|
|
277
|
+
)
|
|
278
|
+
)
|
|
279
|
+
|
|
280
|
+
# Check for certificate trust pinning
|
|
281
|
+
if re.search(r"certificate|cert|X509|trust", content, re.IGNORECASE):
|
|
282
|
+
if not re.search(
|
|
283
|
+
r"pin|trust_store|ca_bundle|certificate_file|verify.*cert",
|
|
284
|
+
content,
|
|
285
|
+
re.IGNORECASE,
|
|
286
|
+
):
|
|
287
|
+
findings.append(
|
|
288
|
+
make_finding(
|
|
289
|
+
Severity.MEDIUM,
|
|
290
|
+
rel,
|
|
291
|
+
0,
|
|
292
|
+
"SAML-09: Certificate trust not pinned",
|
|
293
|
+
"SAML certificate handling without explicit trust pinning. May accept forged certificates.",
|
|
294
|
+
suggestion="Pin IdP certificate or use a trusted CA bundle for validation",
|
|
295
|
+
cwe="CWE-295",
|
|
296
|
+
control_id="SAML-09",
|
|
297
|
+
)
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
# Phase 2: Scan YAML/JSON configs for SAML metadata
|
|
301
|
+
for fpath in safe_rglob(inp.root, "*.xml"):
|
|
302
|
+
files_scanned += 1
|
|
303
|
+
try:
|
|
304
|
+
content = fpath.read_text(encoding="utf-8", errors="replace")
|
|
305
|
+
except Exception as e:
|
|
306
|
+
_log.warning("SamlSSOAgent._run failed: %s", e)
|
|
307
|
+
continue
|
|
308
|
+
|
|
309
|
+
rel = str(fpath.relative_to(inp.root))
|
|
310
|
+
|
|
311
|
+
# Check for SAML metadata files
|
|
312
|
+
if re.search(r"EntityDescriptor|SPSSODescriptor|IDPSSODescriptor", content):
|
|
313
|
+
# Check for SHA-1 in metadata
|
|
314
|
+
if re.search(r"SHA-1|sha1|DigestMethod.*SHA-1", content):
|
|
315
|
+
findings.append(
|
|
316
|
+
make_finding(
|
|
317
|
+
Severity.HIGH,
|
|
318
|
+
rel,
|
|
319
|
+
0,
|
|
320
|
+
"SAML-03: Weak algorithm in SAML metadata",
|
|
321
|
+
"SAML metadata specifies SHA-1 digest method. Upgrade to SHA-256.",
|
|
322
|
+
suggestion="Update DigestMethod and SignatureMethod to SHA-256 or stronger",
|
|
323
|
+
cwe="CWE-327",
|
|
324
|
+
control_id="SAML-03",
|
|
325
|
+
)
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
# Check for WantAssertionsSigned
|
|
329
|
+
if re.search(r"WantAssertionsSigned\s*=\s*[\"']false[\"']", content):
|
|
330
|
+
findings.append(
|
|
331
|
+
make_finding(
|
|
332
|
+
Severity.HIGH,
|
|
333
|
+
rel,
|
|
334
|
+
0,
|
|
335
|
+
"SAML-04: Assertions not required to be signed",
|
|
336
|
+
"WantAssertionsSigned=false allows unsigned assertions. Forged assertions accepted.",
|
|
337
|
+
suggestion="Set WantAssertionsSigned=true in SP metadata",
|
|
338
|
+
cwe="CWE-347",
|
|
339
|
+
control_id="SAML-04",
|
|
340
|
+
)
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
# Phase 3: Try external SAST tools (semgrep has SAML rules)
|
|
344
|
+
semgrep_path = shutil.which("semgrep")
|
|
345
|
+
if semgrep_path:
|
|
346
|
+
from .security_config import _semgrep_config_value
|
|
347
|
+
semgrep_config = _semgrep_config_value(inp.root)
|
|
348
|
+
if not semgrep_config:
|
|
349
|
+
semgrep_path = None # offline + no local pack -> skip
|
|
350
|
+
if semgrep_path:
|
|
351
|
+
try:
|
|
352
|
+
proc = subprocess.run(
|
|
353
|
+
[
|
|
354
|
+
semgrep_path,
|
|
355
|
+
"--config",
|
|
356
|
+
semgrep_config,
|
|
357
|
+
"--include",
|
|
358
|
+
"*.py",
|
|
359
|
+
"--json",
|
|
360
|
+
"--quiet",
|
|
361
|
+
str(inp.root),
|
|
362
|
+
],
|
|
363
|
+
capture_output=True,
|
|
364
|
+
text=True,
|
|
365
|
+
timeout=120,
|
|
366
|
+
)
|
|
367
|
+
if proc.returncode == 0 and proc.stdout:
|
|
368
|
+
import json
|
|
369
|
+
|
|
370
|
+
try:
|
|
371
|
+
data = json.loads(proc.stdout)
|
|
372
|
+
for r in data.get("results", []):
|
|
373
|
+
rule_id = r.get("check_id", "")
|
|
374
|
+
if (
|
|
375
|
+
"saml" in rule_id.lower()
|
|
376
|
+
or "xxe" in rule_id.lower()
|
|
377
|
+
or "xml" in rule_id.lower()
|
|
378
|
+
):
|
|
379
|
+
findings.append(
|
|
380
|
+
make_finding(
|
|
381
|
+
Severity.HIGH,
|
|
382
|
+
r.get("path", ""),
|
|
383
|
+
r.get("start", {}).get("line", 0),
|
|
384
|
+
f"Semgrep: {rule_id}",
|
|
385
|
+
r.get("extra", {}).get("message", ""),
|
|
386
|
+
code_snippet=r.get("extra", {}).get("lines", ""),
|
|
387
|
+
suggestion="Review semgrep finding for SAML security impact",
|
|
388
|
+
)
|
|
389
|
+
)
|
|
390
|
+
except json.JSONDecodeError:
|
|
391
|
+
pass
|
|
392
|
+
except (subprocess.TimeoutExpired, Exception):
|
|
393
|
+
pass
|
|
394
|
+
|
|
395
|
+
result.findings = findings
|
|
396
|
+
result.files_scanned = files_scanned
|
|
397
|
+
result.status = AgentStatus.DONE
|
|
398
|
+
trace.findings = len(findings)
|
|
399
|
+
trace.files_scanned = files_scanned
|
|
400
|
+
|
|
401
|
+
return
|