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,378 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Threat Model Generator — Runtime generator from YAML attack scenarios.
|
|
3
|
+
|
|
4
|
+
Reads attack scenario YAML files and generates executable threat models
|
|
5
|
+
for the current project based on:
|
|
6
|
+
- Detected frameworks and patterns
|
|
7
|
+
- Route analysis
|
|
8
|
+
- Authentication mechanisms
|
|
9
|
+
- Data flow patterns
|
|
10
|
+
|
|
11
|
+
Output: ThreatModel with prioritized attack scenarios for the project.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
from dataclasses import dataclass, field
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
_log = logging.getLogger("patchi.security.threat_model")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class AttackScenario:
|
|
25
|
+
"""A single attack scenario from YAML."""
|
|
26
|
+
|
|
27
|
+
id: str
|
|
28
|
+
name: str
|
|
29
|
+
category: str
|
|
30
|
+
severity: str
|
|
31
|
+
cwe: str
|
|
32
|
+
owasp: str
|
|
33
|
+
description: str
|
|
34
|
+
prerequisites: list[str] = field(default_factory=list)
|
|
35
|
+
attack_steps: list[dict] = field(default_factory=list)
|
|
36
|
+
detection_signatures: list[str] = field(default_factory=list)
|
|
37
|
+
remediation_playbook: str = ""
|
|
38
|
+
tags: list[str] = field(default_factory=list)
|
|
39
|
+
applicable: bool = False
|
|
40
|
+
relevance_score: float = 0.0
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class ThreatModel:
|
|
45
|
+
"""Generated threat model for the project."""
|
|
46
|
+
|
|
47
|
+
project_root: str
|
|
48
|
+
total_scenarios: int = 0
|
|
49
|
+
applicable_scenarios: int = 0
|
|
50
|
+
scenarios: list[AttackScenario] = field(default_factory=list)
|
|
51
|
+
by_severity: dict[str, int] = field(default_factory=dict)
|
|
52
|
+
by_category: dict[str, int] = field(default_factory=dict)
|
|
53
|
+
recommendations: list[str] = field(default_factory=list)
|
|
54
|
+
|
|
55
|
+
def to_dict(self) -> dict:
|
|
56
|
+
return {
|
|
57
|
+
"project_root": self.project_root,
|
|
58
|
+
"total_scenarios": self.total_scenarios,
|
|
59
|
+
"applicable_scenarios": self.applicable_scenarios,
|
|
60
|
+
"by_severity": self.by_severity,
|
|
61
|
+
"by_category": self.by_category,
|
|
62
|
+
"scenarios": [
|
|
63
|
+
{
|
|
64
|
+
"id": s.id,
|
|
65
|
+
"name": s.name,
|
|
66
|
+
"category": s.category,
|
|
67
|
+
"severity": s.severity,
|
|
68
|
+
"cwe": s.cwe,
|
|
69
|
+
"description": s.description,
|
|
70
|
+
"applicable": s.applicable,
|
|
71
|
+
"relevance_score": s.relevance_score,
|
|
72
|
+
"tags": s.tags,
|
|
73
|
+
}
|
|
74
|
+
for s in self.scenarios
|
|
75
|
+
],
|
|
76
|
+
"recommendations": self.recommendations,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# ── Category Detection Rules ─────────────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
# Maps YAML scenario categories to code signals that indicate applicability
|
|
83
|
+
_CATEGORY_SIGNALS: dict[str, list[tuple[str, float]]] = {
|
|
84
|
+
"authentication": [
|
|
85
|
+
("login", 0.8),
|
|
86
|
+
("auth", 0.9),
|
|
87
|
+
("session", 0.7),
|
|
88
|
+
("jwt", 0.9),
|
|
89
|
+
("cookie", 0.6),
|
|
90
|
+
("password", 0.8),
|
|
91
|
+
("bcrypt", 0.8),
|
|
92
|
+
("argon2", 0.8),
|
|
93
|
+
],
|
|
94
|
+
"authorization": [
|
|
95
|
+
("permission", 0.8),
|
|
96
|
+
("role", 0.7),
|
|
97
|
+
("admin", 0.8),
|
|
98
|
+
("access_control", 0.9),
|
|
99
|
+
("guard", 0.6),
|
|
100
|
+
("protect", 0.5),
|
|
101
|
+
],
|
|
102
|
+
"injection": [
|
|
103
|
+
("execute", 0.7),
|
|
104
|
+
("query", 0.6),
|
|
105
|
+
("raw_sql", 0.9),
|
|
106
|
+
("cursor", 0.7),
|
|
107
|
+
("database", 0.5),
|
|
108
|
+
("orm", 0.4),
|
|
109
|
+
("subprocess", 0.8),
|
|
110
|
+
("shell", 0.8),
|
|
111
|
+
("eval", 0.9),
|
|
112
|
+
],
|
|
113
|
+
"xss": [
|
|
114
|
+
("render", 0.5),
|
|
115
|
+
("template", 0.5),
|
|
116
|
+
("innerHTML", 0.9),
|
|
117
|
+
("dangerouslySetInnerHTML", 0.9),
|
|
118
|
+
("markup", 0.6),
|
|
119
|
+
("html", 0.4),
|
|
120
|
+
("response", 0.4),
|
|
121
|
+
],
|
|
122
|
+
"csrf": [
|
|
123
|
+
("form", 0.5),
|
|
124
|
+
("post", 0.4),
|
|
125
|
+
("state_change", 0.8),
|
|
126
|
+
("mutate", 0.7),
|
|
127
|
+
("PUT", 0.6),
|
|
128
|
+
("DELETE", 0.6),
|
|
129
|
+
],
|
|
130
|
+
"secrets": [
|
|
131
|
+
("api_key", 0.9),
|
|
132
|
+
("secret", 0.8),
|
|
133
|
+
("token", 0.7),
|
|
134
|
+
("password", 0.7),
|
|
135
|
+
("credential", 0.8),
|
|
136
|
+
("env", 0.5),
|
|
137
|
+
],
|
|
138
|
+
"dependencies": [
|
|
139
|
+
("requirements", 0.7),
|
|
140
|
+
("package.json", 0.7),
|
|
141
|
+
("Cargo.toml", 0.7),
|
|
142
|
+
("go.mod", 0.7),
|
|
143
|
+
("Gemfile", 0.7),
|
|
144
|
+
("composer.json", 0.7),
|
|
145
|
+
],
|
|
146
|
+
"configuration": [
|
|
147
|
+
("config", 0.5),
|
|
148
|
+
("settings", 0.5),
|
|
149
|
+
("debug", 0.8),
|
|
150
|
+
("verbose", 0.6),
|
|
151
|
+
("cors", 0.8),
|
|
152
|
+
("ssl", 0.7),
|
|
153
|
+
],
|
|
154
|
+
"crypto": [
|
|
155
|
+
("encrypt", 0.8),
|
|
156
|
+
("decrypt", 0.8),
|
|
157
|
+
("hash", 0.7),
|
|
158
|
+
("md5", 0.9),
|
|
159
|
+
("sha1", 0.8),
|
|
160
|
+
("aes", 0.7),
|
|
161
|
+
("rsa", 0.7),
|
|
162
|
+
],
|
|
163
|
+
"network": [
|
|
164
|
+
("http", 0.5),
|
|
165
|
+
("request", 0.4),
|
|
166
|
+
("fetch", 0.4),
|
|
167
|
+
("axios", 0.5),
|
|
168
|
+
("websocket", 0.7),
|
|
169
|
+
("ssrf", 0.9),
|
|
170
|
+
],
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class ThreatModelGenerator:
|
|
175
|
+
"""Generates threat models from YAML scenarios + project context."""
|
|
176
|
+
|
|
177
|
+
def __init__(self, root: Path):
|
|
178
|
+
self.root = root
|
|
179
|
+
self._scenarios_dir = Path(__file__).parent / "attack_scenarios"
|
|
180
|
+
|
|
181
|
+
def generate(self) -> ThreatModel:
|
|
182
|
+
"""Generate a threat model for the current project."""
|
|
183
|
+
model = ThreatModel(project_root=str(self.root))
|
|
184
|
+
|
|
185
|
+
# Load all YAML scenarios
|
|
186
|
+
all_scenarios = self._load_scenarios()
|
|
187
|
+
model.total_scenarios = len(all_scenarios)
|
|
188
|
+
|
|
189
|
+
# Get project context
|
|
190
|
+
project_signals = self._collect_project_signals()
|
|
191
|
+
|
|
192
|
+
# Score and filter scenarios
|
|
193
|
+
for scenario in all_scenarios:
|
|
194
|
+
score = self._score_scenario(scenario, project_signals)
|
|
195
|
+
scenario.relevance_score = score
|
|
196
|
+
scenario.applicable = score > 0.1
|
|
197
|
+
model.scenarios.append(scenario)
|
|
198
|
+
|
|
199
|
+
# Sort by relevance
|
|
200
|
+
model.scenarios.sort(key=lambda s: (-s.relevance_score, s.severity))
|
|
201
|
+
|
|
202
|
+
# Compute summaries
|
|
203
|
+
model.applicable_scenarios = sum(1 for s in model.scenarios if s.applicable)
|
|
204
|
+
for s in model.scenarios:
|
|
205
|
+
if s.applicable:
|
|
206
|
+
model.by_severity[s.severity] = model.by_severity.get(s.severity, 0) + 1
|
|
207
|
+
model.by_category[s.category] = model.by_category.get(s.category, 0) + 1
|
|
208
|
+
|
|
209
|
+
# Generate recommendations
|
|
210
|
+
model.recommendations = self._generate_recommendations(model)
|
|
211
|
+
|
|
212
|
+
_log.info(
|
|
213
|
+
"Threat model: %d/%d scenarios applicable",
|
|
214
|
+
model.applicable_scenarios,
|
|
215
|
+
model.total_scenarios,
|
|
216
|
+
)
|
|
217
|
+
return model
|
|
218
|
+
|
|
219
|
+
def _load_scenarios(self) -> list[AttackScenario]:
|
|
220
|
+
"""Load all attack scenarios from YAML files."""
|
|
221
|
+
scenarios = []
|
|
222
|
+
if not self._scenarios_dir.exists():
|
|
223
|
+
return scenarios
|
|
224
|
+
|
|
225
|
+
for yaml_file in self._scenarios_dir.glob("*.yaml"):
|
|
226
|
+
try:
|
|
227
|
+
import yaml
|
|
228
|
+
|
|
229
|
+
with open(yaml_file, encoding="utf-8") as f:
|
|
230
|
+
data = yaml.safe_load(f)
|
|
231
|
+
if not data or "scenarios" not in data:
|
|
232
|
+
continue
|
|
233
|
+
for s in data["scenarios"]:
|
|
234
|
+
scenarios.append(
|
|
235
|
+
AttackScenario(
|
|
236
|
+
id=s.get("id", "unknown"),
|
|
237
|
+
name=s.get("name", "Unknown"),
|
|
238
|
+
category=s.get("category", "unknown"),
|
|
239
|
+
severity=s.get("severity", "medium"),
|
|
240
|
+
cwe=s.get("cwe", ""),
|
|
241
|
+
owasp=s.get("owasp", ""),
|
|
242
|
+
description=s.get("description", ""),
|
|
243
|
+
prerequisites=s.get("prerequisites", []),
|
|
244
|
+
attack_steps=s.get("attack_steps", []),
|
|
245
|
+
detection_signatures=s.get("detection_signatures", []),
|
|
246
|
+
remediation_playbook=s.get("remediation_playbook", ""),
|
|
247
|
+
tags=s.get("tags", []),
|
|
248
|
+
)
|
|
249
|
+
)
|
|
250
|
+
except Exception as e:
|
|
251
|
+
_log.warning("Failed to load %s: %s", yaml_file.name, e)
|
|
252
|
+
return scenarios
|
|
253
|
+
|
|
254
|
+
def _collect_project_signals(self) -> dict[str, float]:
|
|
255
|
+
"""Collect signals from the project to determine applicability."""
|
|
256
|
+
signals: dict[str, float] = {}
|
|
257
|
+
|
|
258
|
+
# Scan source files for keywords
|
|
259
|
+
source_files = []
|
|
260
|
+
for ext in ("*.py", "*.js", "*.ts", "*.jsx", "*.tsx", "*.go", "*.java", "*.rb"):
|
|
261
|
+
source_files.extend(self.root.rglob(ext))
|
|
262
|
+
|
|
263
|
+
# Limit to 500 files for performance
|
|
264
|
+
source_files = source_files[:500]
|
|
265
|
+
|
|
266
|
+
# Collect keyword frequency
|
|
267
|
+
keyword_counts: dict[str, int] = {}
|
|
268
|
+
for fp in source_files:
|
|
269
|
+
# Skip vendor/node_modules/venv
|
|
270
|
+
parts = fp.relative_to(self.root).parts
|
|
271
|
+
if any(
|
|
272
|
+
p.startswith(".") or p in ("node_modules", "__pycache__", ".venv", "venv")
|
|
273
|
+
for p in parts
|
|
274
|
+
):
|
|
275
|
+
continue
|
|
276
|
+
try:
|
|
277
|
+
content = fp.read_text(encoding="utf-8", errors="ignore").lower()
|
|
278
|
+
for _category, kws in _CATEGORY_SIGNALS.items():
|
|
279
|
+
for kw, _ in kws:
|
|
280
|
+
if kw.lower() in content:
|
|
281
|
+
keyword_counts[kw] = keyword_counts.get(kw, 0) + 1
|
|
282
|
+
except Exception as _exc:
|
|
283
|
+
_log.warning('_collect_project_signals failed: %s', _exc)
|
|
284
|
+
|
|
285
|
+
# Convert counts to scores (0-1)
|
|
286
|
+
max_count = max(keyword_counts.values()) if keyword_counts else 1
|
|
287
|
+
for kw, count in keyword_counts.items():
|
|
288
|
+
signals[kw] = min(count / max(max_count, 1), 1.0)
|
|
289
|
+
|
|
290
|
+
# Check for dependency files
|
|
291
|
+
dep_files = [
|
|
292
|
+
"requirements.txt",
|
|
293
|
+
"package.json",
|
|
294
|
+
"Cargo.toml",
|
|
295
|
+
"go.mod",
|
|
296
|
+
"Gemfile",
|
|
297
|
+
"composer.json",
|
|
298
|
+
"Pipfile",
|
|
299
|
+
]
|
|
300
|
+
for df in dep_files:
|
|
301
|
+
if (self.root / df).exists():
|
|
302
|
+
signals[df] = 1.0
|
|
303
|
+
|
|
304
|
+
# Check for config files
|
|
305
|
+
config_files = [
|
|
306
|
+
"config.py",
|
|
307
|
+
"settings.py",
|
|
308
|
+
".env",
|
|
309
|
+
"config.yaml",
|
|
310
|
+
"config.json",
|
|
311
|
+
"application.properties",
|
|
312
|
+
]
|
|
313
|
+
for cf in config_files:
|
|
314
|
+
if list(self.root.rglob(cf)):
|
|
315
|
+
signals[cf] = 1.0
|
|
316
|
+
|
|
317
|
+
return signals
|
|
318
|
+
|
|
319
|
+
def _score_scenario(self, scenario: AttackScenario, signals: dict[str, float]) -> float:
|
|
320
|
+
"""Score a scenario based on project signals."""
|
|
321
|
+
category_signals = _CATEGORY_SIGNALS.get(scenario.category, [])
|
|
322
|
+
if not category_signals:
|
|
323
|
+
return 0.2 # Default low relevance for unknown categories
|
|
324
|
+
|
|
325
|
+
score = 0.0
|
|
326
|
+
for kw, weight in category_signals:
|
|
327
|
+
signal_value = signals.get(kw, 0.0)
|
|
328
|
+
score += signal_value * weight
|
|
329
|
+
|
|
330
|
+
# Normalize to 0-1
|
|
331
|
+
max_possible = sum(w for _, w in category_signals)
|
|
332
|
+
if max_possible > 0:
|
|
333
|
+
score = min(score / max_possible, 1.0)
|
|
334
|
+
|
|
335
|
+
# Boost for CWE tags matching project patterns
|
|
336
|
+
if scenario.cwe:
|
|
337
|
+
cwe_num = scenario.cwe.replace("CWE-", "")
|
|
338
|
+
# SQL injection CWEs get boost if project uses SQL
|
|
339
|
+
if cwe_num in ("89", "90") and signals.get("execute", 0) > 0.5:
|
|
340
|
+
score = min(score * 1.5, 1.0)
|
|
341
|
+
# XSS CWEs get boost if project renders HTML
|
|
342
|
+
if cwe_num in ("79", "80") and signals.get("render", 0) > 0.3:
|
|
343
|
+
score = min(score * 1.3, 1.0)
|
|
344
|
+
|
|
345
|
+
return score
|
|
346
|
+
|
|
347
|
+
def _generate_recommendations(self, model: ThreatModel) -> list[str]:
|
|
348
|
+
"""Generate security recommendations based on the threat model."""
|
|
349
|
+
recs = []
|
|
350
|
+
|
|
351
|
+
# High-severity applicable scenarios
|
|
352
|
+
high_sev = [
|
|
353
|
+
s for s in model.scenarios if s.applicable and s.severity in ("critical", "high")
|
|
354
|
+
]
|
|
355
|
+
if high_sev:
|
|
356
|
+
recs.append(
|
|
357
|
+
f"Address {len(high_sev)} high/critical scenarios: "
|
|
358
|
+
+ ", ".join(s.name for s in high_sev[:3])
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
# Category gaps
|
|
362
|
+
if "secrets" in model.by_category:
|
|
363
|
+
recs.append("Review secret management — multiple secret-related scenarios apply")
|
|
364
|
+
|
|
365
|
+
if "injection" in model.by_category:
|
|
366
|
+
recs.append("Validate all user inputs — injection scenarios detected")
|
|
367
|
+
|
|
368
|
+
if "auth" in str(model.by_category):
|
|
369
|
+
recs.append("Audit authentication flows — auth scenarios are relevant")
|
|
370
|
+
|
|
371
|
+
# Dependency warnings
|
|
372
|
+
dep_scenarios = [
|
|
373
|
+
s for s in model.scenarios if s.category == "dependencies" and s.applicable
|
|
374
|
+
]
|
|
375
|
+
if dep_scenarios:
|
|
376
|
+
recs.append("Run dependency vulnerability scan — project has dependency files")
|
|
377
|
+
|
|
378
|
+
return recs
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Threat Model Updater — refines threat model from actual scan findings.
|
|
3
|
+
|
|
4
|
+
After a scan completes, this module:
|
|
5
|
+
1. Boosts scenarios that match confirmed findings
|
|
6
|
+
2. Adds new scenarios for finding types not in the YAML library
|
|
7
|
+
3. Deprioritizes scenarios that produced no findings (false-alarm decay)
|
|
8
|
+
4. Persists the updated model for the next scan cycle
|
|
9
|
+
|
|
10
|
+
The result is a threat model that adapts to the real risk profile of the
|
|
11
|
+
project rather than relying solely on static YAML definitions.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import logging
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any
|
|
20
|
+
|
|
21
|
+
from patchi.core.security.threat_model_generator import (
|
|
22
|
+
AttackScenario,
|
|
23
|
+
ThreatModel,
|
|
24
|
+
ThreatModelGenerator,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
_log = logging.getLogger("patchi.security.threat_model_updater")
|
|
28
|
+
|
|
29
|
+
# How much to boost a scenario's score when a finding matches its category
|
|
30
|
+
_FINDING_BOOST = 0.15
|
|
31
|
+
# Maximum boosted score
|
|
32
|
+
_MAX_SCORE = 1.0
|
|
33
|
+
# Decay factor for scenarios with zero findings (per scan cycle)
|
|
34
|
+
_ZERO_FINDING_DECAY = 0.05
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def update_threat_model(
|
|
38
|
+
root: Path,
|
|
39
|
+
findings: list[dict[str, Any]],
|
|
40
|
+
) -> ThreatModel:
|
|
41
|
+
"""Regenerate the threat model and adjust scores based on scan findings.
|
|
42
|
+
|
|
43
|
+
Returns the updated ThreatModel.
|
|
44
|
+
"""
|
|
45
|
+
gen = ThreatModelGenerator(root)
|
|
46
|
+
model = gen.generate()
|
|
47
|
+
|
|
48
|
+
# Index findings by category
|
|
49
|
+
category_counts: dict[str, int] = {}
|
|
50
|
+
cwe_counts: dict[str, int] = {}
|
|
51
|
+
finding_types: set[str] = set()
|
|
52
|
+
for f in findings:
|
|
53
|
+
cat = _categorize_finding(f)
|
|
54
|
+
category_counts[cat] = category_counts.get(cat, 0) + 1
|
|
55
|
+
cwe = f.get("cwe", "")
|
|
56
|
+
if cwe:
|
|
57
|
+
cwe_counts[cwe] = cwe_counts.get(cwe, 0) + 1
|
|
58
|
+
finding_types.add(f.get("type", f.get("control_id", "")))
|
|
59
|
+
|
|
60
|
+
# Boost scenarios matching actual findings
|
|
61
|
+
for scenario in model.scenarios:
|
|
62
|
+
if not scenario.applicable:
|
|
63
|
+
continue
|
|
64
|
+
cat_count = category_counts.get(scenario.category, 0)
|
|
65
|
+
if cat_count > 0:
|
|
66
|
+
boost = min(cat_count * _FINDING_BOOST, _MAX_SCORE - scenario.relevance_score)
|
|
67
|
+
scenario.relevance_score = min(scenario.relevance_score + boost, _MAX_SCORE)
|
|
68
|
+
_log.debug(
|
|
69
|
+
"Boosted %s by %.2f (category=%s, findings=%d)",
|
|
70
|
+
scenario.id,
|
|
71
|
+
boost,
|
|
72
|
+
scenario.category,
|
|
73
|
+
cat_count,
|
|
74
|
+
)
|
|
75
|
+
else:
|
|
76
|
+
# Decay scenarios that produced zero findings
|
|
77
|
+
scenario.relevance_score = max(scenario.relevance_score - _ZERO_FINDING_DECAY, 0.0)
|
|
78
|
+
|
|
79
|
+
# Extra boost for CWE matches
|
|
80
|
+
if scenario.cwe and cwe_counts.get(scenario.cwe, 0) > 0:
|
|
81
|
+
scenario.relevance_score = min(scenario.relevance_score + 0.2, _MAX_SCORE)
|
|
82
|
+
|
|
83
|
+
# Re-sort by updated relevance
|
|
84
|
+
model.scenarios.sort(key=lambda s: (-s.relevance_score, s.severity))
|
|
85
|
+
|
|
86
|
+
# Recompute summaries
|
|
87
|
+
model.applicable_scenarios = sum(1 for s in model.scenarios if s.applicable)
|
|
88
|
+
model.by_severity.clear()
|
|
89
|
+
model.by_category.clear()
|
|
90
|
+
for s in model.scenarios:
|
|
91
|
+
if s.applicable:
|
|
92
|
+
model.by_severity[s.severity] = model.by_severity.get(s.severity, 0) + 1
|
|
93
|
+
model.by_category[s.category] = model.by_category.get(s.category, 0) + 1
|
|
94
|
+
|
|
95
|
+
# Add discovered scenarios for finding types not in the YAML library
|
|
96
|
+
_add_discovered_scenarios(model, finding_types, category_counts)
|
|
97
|
+
|
|
98
|
+
# Update recommendations
|
|
99
|
+
model.recommendations = _generate_updated_recommendations(model, category_counts)
|
|
100
|
+
|
|
101
|
+
# Persist
|
|
102
|
+
tm_path = root / ".patchi" / "threat_model.json"
|
|
103
|
+
tm_path.parent.mkdir(parents=True, exist_ok=True)
|
|
104
|
+
tm_path.write_text(json.dumps(model.to_dict(), indent=2), encoding="utf-8")
|
|
105
|
+
_log.info(
|
|
106
|
+
"Threat model updated: %d scenarios, %d applicable, %d categories",
|
|
107
|
+
model.total_scenarios,
|
|
108
|
+
model.applicable_scenarios,
|
|
109
|
+
len(model.by_category),
|
|
110
|
+
)
|
|
111
|
+
return model
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _categorize_finding(finding: dict[str, Any]) -> str:
|
|
115
|
+
"""Map a finding to a threat model category."""
|
|
116
|
+
ftype = (finding.get("type", "") or finding.get("control_id", "")).lower()
|
|
117
|
+
categories = {
|
|
118
|
+
"authentication": ("auth", "login", "session", "password", "jwt", "cookie"),
|
|
119
|
+
"authorization": ("permission", "role", "admin", "access", "privilege"),
|
|
120
|
+
"injection": ("sql", "injection", "command", "eval", "exec", "shell", "xss"),
|
|
121
|
+
"secrets": ("secret", "credential", "api_key", "token", "hardcoded"),
|
|
122
|
+
"crypto": ("hash", "md5", "sha1", "encrypt", "decrypt", "cipher"),
|
|
123
|
+
"configuration": ("config", "debug", "cors", "ssl", "tls", "verbose"),
|
|
124
|
+
"network": ("ssrf", "redirect", "request", "fetch", "url"),
|
|
125
|
+
"dependencies": ("dependency", "vulnerability", "outdated", "cve"),
|
|
126
|
+
}
|
|
127
|
+
for cat, keywords in categories.items():
|
|
128
|
+
if any(kw in ftype for kw in keywords):
|
|
129
|
+
return cat
|
|
130
|
+
return "other"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _add_discovered_scenarios(
|
|
134
|
+
model: ThreatModel,
|
|
135
|
+
finding_types: set[str],
|
|
136
|
+
category_counts: dict[str, int],
|
|
137
|
+
) -> None:
|
|
138
|
+
"""Add synthetic scenarios for finding types not covered by YAML library."""
|
|
139
|
+
existing_ids = {s.id for s in model.scenarios}
|
|
140
|
+
discovered = 0
|
|
141
|
+
|
|
142
|
+
for ftype in finding_types:
|
|
143
|
+
if not ftype or ftype in existing_ids:
|
|
144
|
+
continue
|
|
145
|
+
cat = _categorize_finding({"type": ftype})
|
|
146
|
+
if cat == "other":
|
|
147
|
+
continue
|
|
148
|
+
# Only add if we have multiple findings of this type
|
|
149
|
+
if category_counts.get(cat, 0) < 2:
|
|
150
|
+
continue
|
|
151
|
+
synthetic_id = f"DISCOVERED-{ftype.upper()[:20]}"
|
|
152
|
+
if synthetic_id in existing_ids:
|
|
153
|
+
continue
|
|
154
|
+
scenario = AttackScenario(
|
|
155
|
+
id=synthetic_id,
|
|
156
|
+
name=f"Discovered: {ftype}",
|
|
157
|
+
category=cat,
|
|
158
|
+
severity="medium",
|
|
159
|
+
cwe="",
|
|
160
|
+
owasp="",
|
|
161
|
+
description=f"Auto-discovered from {category_counts[cat]} scan findings of type '{ftype}'",
|
|
162
|
+
tags=["discovered", "auto"],
|
|
163
|
+
applicable=True,
|
|
164
|
+
relevance_score=min(0.3 + category_counts.get(cat, 0) * 0.05, 0.8),
|
|
165
|
+
)
|
|
166
|
+
model.scenarios.append(scenario)
|
|
167
|
+
existing_ids.add(synthetic_id)
|
|
168
|
+
discovered += 1
|
|
169
|
+
|
|
170
|
+
model.total_scenarios += discovered
|
|
171
|
+
if discovered:
|
|
172
|
+
_log.info("Added %d discovered scenarios from scan findings", discovered)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _generate_updated_recommendations(
|
|
176
|
+
model: ThreatModel,
|
|
177
|
+
category_counts: dict[str, int],
|
|
178
|
+
) -> list[str]:
|
|
179
|
+
"""Generate recommendations factoring in actual findings."""
|
|
180
|
+
recs = list(model.recommendations)
|
|
181
|
+
|
|
182
|
+
# Top-finding categories
|
|
183
|
+
top_cats = sorted(category_counts.items(), key=lambda x: -x[1])[:3]
|
|
184
|
+
for cat, count in top_cats:
|
|
185
|
+
if count >= 3:
|
|
186
|
+
recs.append(f"High volume of {cat} findings ({count}) — prioritize remediation")
|
|
187
|
+
|
|
188
|
+
# Categories with no coverage
|
|
189
|
+
covered = set(model.by_category.keys())
|
|
190
|
+
for cat in category_counts:
|
|
191
|
+
if cat not in covered and category_counts[cat] >= 2:
|
|
192
|
+
recs.append(
|
|
193
|
+
f"Add threat scenarios for '{cat}' — {category_counts[cat]} findings detected"
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
return recs[:10]
|