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,236 @@
|
|
|
1
|
+
"""StateFuzzer — explore application state machine transitions.
|
|
2
|
+
|
|
3
|
+
Given an AssuranceGraph with states and transitions, generates test
|
|
4
|
+
sequences that attempt illegal state transitions, replay attacks,
|
|
5
|
+
and concurrent state mutations.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
|
|
12
|
+
from patchi.core.assurance.graph import AssuranceGraph
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class StateTransition:
|
|
17
|
+
"""A single state transition."""
|
|
18
|
+
|
|
19
|
+
from_state: str
|
|
20
|
+
to_state: str
|
|
21
|
+
action: str # trigger/action name
|
|
22
|
+
|
|
23
|
+
def to_dict(self) -> dict:
|
|
24
|
+
return {
|
|
25
|
+
"from": self.from_state,
|
|
26
|
+
"to": self.to_state,
|
|
27
|
+
"action": self.action,
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class FuzzPath:
|
|
33
|
+
"""A path through the state machine for testing."""
|
|
34
|
+
|
|
35
|
+
label: str
|
|
36
|
+
transitions: list[StateTransition]
|
|
37
|
+
strategy: str # "illegal", "replay", "boundary", "concurrent"
|
|
38
|
+
description: str = ""
|
|
39
|
+
|
|
40
|
+
def to_dict(self) -> dict:
|
|
41
|
+
return {
|
|
42
|
+
"label": self.label,
|
|
43
|
+
"transitions": [t.to_dict() for t in self.transitions],
|
|
44
|
+
"strategy": self.strategy,
|
|
45
|
+
"description": self.description,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class StateFuzzer:
|
|
50
|
+
"""Generate state-machine fuzz paths from an AssuranceGraph."""
|
|
51
|
+
|
|
52
|
+
def __init__(self, graph: AssuranceGraph | None = None):
|
|
53
|
+
self._graph = graph
|
|
54
|
+
self._transitions: list[StateTransition] = []
|
|
55
|
+
self._states: set[str] = set()
|
|
56
|
+
self._legal_map: dict[str, list[str]] = {}
|
|
57
|
+
|
|
58
|
+
def load_graph(self, graph: AssuranceGraph) -> None:
|
|
59
|
+
"""Load transitions from an assurance graph's data."""
|
|
60
|
+
self._graph = graph
|
|
61
|
+
self._extract_transitions()
|
|
62
|
+
|
|
63
|
+
def _extract_transitions(self) -> None:
|
|
64
|
+
"""Extract state transitions from the graph."""
|
|
65
|
+
if self._graph is None:
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
self._transitions = []
|
|
69
|
+
self._states = set()
|
|
70
|
+
self._legal_map = {}
|
|
71
|
+
|
|
72
|
+
for claim in self._graph.claims.values():
|
|
73
|
+
# Extract states and transitions from claim evidence
|
|
74
|
+
for ev in claim.evidence:
|
|
75
|
+
artifact = ev.artifact
|
|
76
|
+
if "from_state" in artifact and "to_state" in artifact:
|
|
77
|
+
t = StateTransition(
|
|
78
|
+
from_state=artifact["from_state"],
|
|
79
|
+
to_state=artifact["to_state"],
|
|
80
|
+
action=artifact.get("action", "unknown"),
|
|
81
|
+
)
|
|
82
|
+
self._transitions.append(t)
|
|
83
|
+
self._states.add(t.from_state)
|
|
84
|
+
self._states.add(t.to_state)
|
|
85
|
+
self._legal_map.setdefault(t.from_state, []).append(t.to_state)
|
|
86
|
+
|
|
87
|
+
def load_from_dict(self, data: dict) -> None:
|
|
88
|
+
"""Load transitions from a dict (e.g. from brain data)."""
|
|
89
|
+
self._transitions = []
|
|
90
|
+
self._states = set()
|
|
91
|
+
self._legal_map = {}
|
|
92
|
+
|
|
93
|
+
for t_data in data.get("transitions", []):
|
|
94
|
+
t = StateTransition(
|
|
95
|
+
from_state=t_data["from_state"],
|
|
96
|
+
to_state=t_data["to_state"],
|
|
97
|
+
action=t_data.get("action", "unknown"),
|
|
98
|
+
)
|
|
99
|
+
self._transitions.append(t)
|
|
100
|
+
self._states.add(t.from_state)
|
|
101
|
+
self._states.add(t.to_state)
|
|
102
|
+
self._legal_map.setdefault(t.from_state, []).append(t.to_state)
|
|
103
|
+
|
|
104
|
+
def generate(self, max_paths: int = 30) -> list[FuzzPath]:
|
|
105
|
+
"""Generate fuzz paths through the state machine."""
|
|
106
|
+
results: list[FuzzPath] = []
|
|
107
|
+
|
|
108
|
+
if not self._states:
|
|
109
|
+
return results
|
|
110
|
+
|
|
111
|
+
# 1. Illegal transitions (try going to every state from every state)
|
|
112
|
+
results.extend(self._illegal_transitions())
|
|
113
|
+
|
|
114
|
+
# 2. Replay attack (repeat same transition)
|
|
115
|
+
results.extend(self._replay_attacks())
|
|
116
|
+
|
|
117
|
+
# 3. Boundary paths (start→end shortest paths, then extend)
|
|
118
|
+
results.extend(self._boundary_paths())
|
|
119
|
+
|
|
120
|
+
# 4. Missing guards (states with many outgoing transitions)
|
|
121
|
+
results.extend(self._unguarded_transitions())
|
|
122
|
+
|
|
123
|
+
return results[:max_paths]
|
|
124
|
+
|
|
125
|
+
def _illegal_transitions(self) -> list[FuzzPath]:
|
|
126
|
+
"""Try transitions that should not be legal."""
|
|
127
|
+
results: list[FuzzPath] = []
|
|
128
|
+
for from_s in self._states:
|
|
129
|
+
legal_targets = set(self._legal_map.get(from_s, []))
|
|
130
|
+
for to_s in self._states:
|
|
131
|
+
if to_s != from_s and to_s not in legal_targets:
|
|
132
|
+
results.append(
|
|
133
|
+
FuzzPath(
|
|
134
|
+
label=f"illegal_{from_s}_to_{to_s}",
|
|
135
|
+
transitions=[StateTransition(from_s, to_s, "test_illegal")],
|
|
136
|
+
strategy="illegal",
|
|
137
|
+
description=f"Attempt illegal transition: {from_s} → {to_s}",
|
|
138
|
+
)
|
|
139
|
+
)
|
|
140
|
+
return results
|
|
141
|
+
|
|
142
|
+
def _replay_attacks(self) -> list[FuzzPath]:
|
|
143
|
+
"""Replay the same transition multiple times."""
|
|
144
|
+
results: list[FuzzPath] = []
|
|
145
|
+
for t in self._transitions:
|
|
146
|
+
results.append(
|
|
147
|
+
FuzzPath(
|
|
148
|
+
label=f"replay_{t.from_state}_{t.to_state}_x3",
|
|
149
|
+
transitions=[t, t, t],
|
|
150
|
+
strategy="replay",
|
|
151
|
+
description=f"Replay {t.action} ({t.from_state}→{t.to_state}) 3 times",
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
return results
|
|
155
|
+
|
|
156
|
+
def _boundary_paths(self) -> list[FuzzPath]:
|
|
157
|
+
"""Find longest paths through the state machine."""
|
|
158
|
+
results: list[FuzzPath] = []
|
|
159
|
+
if not self._transitions:
|
|
160
|
+
return results
|
|
161
|
+
|
|
162
|
+
# Find start states (no incoming transitions)
|
|
163
|
+
all_targets = {t.to_state for t in self._transitions}
|
|
164
|
+
start_states = self._states - all_targets
|
|
165
|
+
if not start_states:
|
|
166
|
+
start_states = {self._transitions[0].from_state}
|
|
167
|
+
|
|
168
|
+
# DFS to find paths
|
|
169
|
+
for start in start_states:
|
|
170
|
+
paths = self._dfs_paths(start, max_depth=6, max_results=5)
|
|
171
|
+
for path in paths:
|
|
172
|
+
results.append(
|
|
173
|
+
FuzzPath(
|
|
174
|
+
label=f"boundary_path_{'_'.join(t.from_state for t in path)}",
|
|
175
|
+
transitions=path,
|
|
176
|
+
strategy="boundary",
|
|
177
|
+
description=f"Path: {' → '.join(t.from_state for t in path)} → {path[-1].to_state}",
|
|
178
|
+
)
|
|
179
|
+
)
|
|
180
|
+
return results
|
|
181
|
+
|
|
182
|
+
def _dfs_paths(
|
|
183
|
+
self, start: str, max_depth: int = 6, max_results: int = 5
|
|
184
|
+
) -> list[list[StateTransition]]:
|
|
185
|
+
"""DFS to find paths through the state machine."""
|
|
186
|
+
results: list[list[StateTransition]] = []
|
|
187
|
+
stack: list[tuple[str, list[StateTransition], set[str]]] = [(start, [], {start})]
|
|
188
|
+
|
|
189
|
+
while stack and len(results) < max_results:
|
|
190
|
+
state, path, visited = stack.pop()
|
|
191
|
+
targets = self._legal_map.get(state, [])
|
|
192
|
+
for target in targets:
|
|
193
|
+
if target in visited:
|
|
194
|
+
continue
|
|
195
|
+
transition = next(
|
|
196
|
+
(
|
|
197
|
+
t
|
|
198
|
+
for t in self._transitions
|
|
199
|
+
if t.from_state == state and t.to_state == target
|
|
200
|
+
),
|
|
201
|
+
StateTransition(state, target, "unknown"),
|
|
202
|
+
)
|
|
203
|
+
new_path = path + [transition]
|
|
204
|
+
if len(new_path) >= max_depth:
|
|
205
|
+
results.append(new_path)
|
|
206
|
+
else:
|
|
207
|
+
stack.append((target, new_path, visited | {target}))
|
|
208
|
+
|
|
209
|
+
return results
|
|
210
|
+
|
|
211
|
+
def _unguarded_transitions(self) -> list[FuzzPath]:
|
|
212
|
+
"""Find states with many outgoing transitions (potential missing guards)."""
|
|
213
|
+
results: list[FuzzPath] = []
|
|
214
|
+
for state, targets in self._legal_map.items():
|
|
215
|
+
if len(targets) >= 3:
|
|
216
|
+
# Try all outgoing transitions in sequence
|
|
217
|
+
transitions = [
|
|
218
|
+
next(
|
|
219
|
+
(
|
|
220
|
+
t
|
|
221
|
+
for t in self._transitions
|
|
222
|
+
if t.from_state == state and t.to_state == tgt
|
|
223
|
+
),
|
|
224
|
+
StateTransition(state, tgt, "unknown"),
|
|
225
|
+
)
|
|
226
|
+
for tgt in targets
|
|
227
|
+
]
|
|
228
|
+
results.append(
|
|
229
|
+
FuzzPath(
|
|
230
|
+
label=f"unguarded_{state}",
|
|
231
|
+
transitions=transitions,
|
|
232
|
+
strategy="boundary",
|
|
233
|
+
description=f"State '{state}' has {len(targets)} outgoing transitions — test all",
|
|
234
|
+
)
|
|
235
|
+
)
|
|
236
|
+
return results
|
patchi/core/health.py
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Patchi Health Score — single 0–100 number computed from scan data.
|
|
3
|
+
|
|
4
|
+
Formula (Section 13 of spec):
|
|
5
|
+
Security posture 35% — findings severity weighted, secret count, vuln deps
|
|
6
|
+
Test coverage 25% — % of source files with a test counterpart
|
|
7
|
+
Dead code ratio 20% — % of files that are confirmed dead
|
|
8
|
+
Dependency health 10% — % of deps with no known CVE
|
|
9
|
+
App contract 10% — % of contract flows confirmed (0 if no contract yet)
|
|
10
|
+
|
|
11
|
+
Score interpretation:
|
|
12
|
+
90–100 Excellent (green)
|
|
13
|
+
70–89 Good (yellow-green)
|
|
14
|
+
50–69 Fair (yellow)
|
|
15
|
+
30–49 Poor (orange)
|
|
16
|
+
0–29 Critical (red)
|
|
17
|
+
|
|
18
|
+
The score is computed from brain memory + agent scan results.
|
|
19
|
+
It is saved back to brain memory after every scan so `p status` can display it.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import logging
|
|
25
|
+
import threading
|
|
26
|
+
import time
|
|
27
|
+
from dataclasses import dataclass
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
from patchi.core import memory as mem
|
|
31
|
+
|
|
32
|
+
_log = logging.getLogger("patchi.core.health")
|
|
33
|
+
|
|
34
|
+
# In-process cache: recompute at most every N seconds so dashboard requests
|
|
35
|
+
# stay fast. Scan completion bumps the timestamp so fresh data shows quickly.
|
|
36
|
+
_HEALTH_TTL = 30.0
|
|
37
|
+
_health_cache: dict = {}
|
|
38
|
+
_health_lock = threading.Lock()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def invalidate_cache() -> None:
|
|
42
|
+
"""Force next compute() to re-run (call after a scan completes)."""
|
|
43
|
+
with _health_lock:
|
|
44
|
+
_health_cache.clear()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class HealthScore:
|
|
49
|
+
total: int # 0–100
|
|
50
|
+
security: float # component raw score 0–100
|
|
51
|
+
test_coverage: float
|
|
52
|
+
dead_code: float
|
|
53
|
+
dependency: float
|
|
54
|
+
contract: float
|
|
55
|
+
grade: str # A / B / C / D / F
|
|
56
|
+
color: str # rich color string
|
|
57
|
+
breakdown: dict # per-component detail
|
|
58
|
+
|
|
59
|
+
def to_dict(self) -> dict:
|
|
60
|
+
return {
|
|
61
|
+
"total": self.total,
|
|
62
|
+
"grade": self.grade,
|
|
63
|
+
"color": self.color,
|
|
64
|
+
"components": {
|
|
65
|
+
"security": round(self.security, 1),
|
|
66
|
+
"test_coverage": round(self.test_coverage, 1),
|
|
67
|
+
"dead_code": round(self.dead_code, 1),
|
|
68
|
+
"dependency": round(self.dependency, 1),
|
|
69
|
+
"contract": round(self.contract, 1),
|
|
70
|
+
},
|
|
71
|
+
"breakdown": self.breakdown,
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# ── Weights ────────────────────────────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
_WEIGHTS = {
|
|
78
|
+
"security": 0.35,
|
|
79
|
+
"test_coverage": 0.25,
|
|
80
|
+
"dead_code": 0.20,
|
|
81
|
+
"dependency": 0.10,
|
|
82
|
+
"contract": 0.10,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# ── Main compute function ──────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def compute(root: Path | None = None) -> HealthScore:
|
|
90
|
+
"""
|
|
91
|
+
Compute the health score from brain memory and agent scan results.
|
|
92
|
+
Returns a HealthScore with all components filled.
|
|
93
|
+
Results are cached in-process for _HEALTH_TTL seconds — the dashboard
|
|
94
|
+
calls this on every request, and a full recompute is expensive.
|
|
95
|
+
"""
|
|
96
|
+
key = str(root) if root is not None else ""
|
|
97
|
+
now = time.time()
|
|
98
|
+
with _health_lock:
|
|
99
|
+
cached = _health_cache.get(key)
|
|
100
|
+
if cached and (now - cached[0]) < _HEALTH_TTL:
|
|
101
|
+
return cached[1]
|
|
102
|
+
|
|
103
|
+
brain = mem.get_brain(root)
|
|
104
|
+
scans = mem.get_scan_results(root)
|
|
105
|
+
patches = mem.list_patches(root)
|
|
106
|
+
|
|
107
|
+
security = _compute_security(brain, scans)
|
|
108
|
+
test_coverage_pct = _compute_test_coverage_pct(root, brain)
|
|
109
|
+
test_coverage = _compute_test_coverage(brain, scans, test_coverage_pct)
|
|
110
|
+
dead_code = _compute_dead_code(brain, scans)
|
|
111
|
+
dependency = _compute_dependency(brain, scans)
|
|
112
|
+
contract = _compute_contract(brain)
|
|
113
|
+
|
|
114
|
+
raw = (
|
|
115
|
+
security * _WEIGHTS["security"]
|
|
116
|
+
+ test_coverage * _WEIGHTS["test_coverage"]
|
|
117
|
+
+ dead_code * _WEIGHTS["dead_code"]
|
|
118
|
+
+ dependency * _WEIGHTS["dependency"]
|
|
119
|
+
+ contract * _WEIGHTS["contract"]
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
total = max(0, min(100, round(raw)))
|
|
123
|
+
grade, color = _grade(total)
|
|
124
|
+
|
|
125
|
+
score = HealthScore(
|
|
126
|
+
total=total,
|
|
127
|
+
security=security,
|
|
128
|
+
test_coverage=test_coverage,
|
|
129
|
+
dead_code=dead_code,
|
|
130
|
+
dependency=dependency,
|
|
131
|
+
contract=contract,
|
|
132
|
+
grade=grade,
|
|
133
|
+
color=color,
|
|
134
|
+
breakdown=_build_breakdown(brain, scans, patches, test_coverage_pct),
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# Save to brain memory so `p status` can display it without rescanning
|
|
138
|
+
try:
|
|
139
|
+
brain["health_score"] = score.to_dict()
|
|
140
|
+
brain["test_coverage_pct"] = test_coverage_pct
|
|
141
|
+
mem.save_brain(brain, root)
|
|
142
|
+
except Exception as e:
|
|
143
|
+
_log.warning("Failed to save health score to brain: %s", e)
|
|
144
|
+
|
|
145
|
+
# Persist health score to scan history for trend charts (WIRE-07)
|
|
146
|
+
if root is not None:
|
|
147
|
+
try:
|
|
148
|
+
from patchi.core.security.history import patchi_record_scan
|
|
149
|
+
|
|
150
|
+
patchi_record_scan(root, tool="health_compute", findings=[], health_score=total)
|
|
151
|
+
except Exception as e:
|
|
152
|
+
_log.warning("Failed to record health score to history: %s", e)
|
|
153
|
+
|
|
154
|
+
with _health_lock:
|
|
155
|
+
_health_cache[key] = (time.time(), score)
|
|
156
|
+
|
|
157
|
+
return score
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# ── Component computers ────────────────────────────────────────────────────────
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _compute_security(brain: dict, scans: dict) -> float:
|
|
164
|
+
"""
|
|
165
|
+
Security score (0-100).
|
|
166
|
+
Deductions:
|
|
167
|
+
CRITICAL finding -20 (capped at -60 total)
|
|
168
|
+
HIGH finding -10 (capped at -30 total)
|
|
169
|
+
MEDIUM finding -3
|
|
170
|
+
Hardcoded secret -25 (per secret, heavy)
|
|
171
|
+
Unprotected route -8 (per route)
|
|
172
|
+
Injection vuln -15 (per finding, capped at -30)
|
|
173
|
+
Crypto issue -5 (per finding, capped at -15)
|
|
174
|
+
AuthZ issue -10 (per finding, capped at -20)
|
|
175
|
+
"""
|
|
176
|
+
score = 100.0
|
|
177
|
+
env_data = scans.get("EnvScanner", {})
|
|
178
|
+
route_data = scans.get("RouteGraphScanner", {})
|
|
179
|
+
|
|
180
|
+
secret_count = env_data.get("finding_count", 0)
|
|
181
|
+
score -= min(secret_count * 25, 60)
|
|
182
|
+
|
|
183
|
+
unprotected = route_data.get("finding_count", 0)
|
|
184
|
+
score -= min(unprotected * 8, 32)
|
|
185
|
+
|
|
186
|
+
circular = len(brain.get("circular_deps", []))
|
|
187
|
+
score -= min(circular * 2, 10)
|
|
188
|
+
|
|
189
|
+
security_agents = [
|
|
190
|
+
"TaintAnalyzer",
|
|
191
|
+
"SecretScanner",
|
|
192
|
+
"ConfigAuditAgent",
|
|
193
|
+
"HeaderAuditAgent",
|
|
194
|
+
"RateLimitAuditor",
|
|
195
|
+
"CORSAuditor",
|
|
196
|
+
"DependencyCVEChecker",
|
|
197
|
+
"MisconfigAgent",
|
|
198
|
+
"JWTSecurityAgent",
|
|
199
|
+
"SensitiveDataAgent",
|
|
200
|
+
"AuthenticationAuditAgent",
|
|
201
|
+
"SSRFProtectionAgent",
|
|
202
|
+
"InjectionAgent",
|
|
203
|
+
"AuthZAgent",
|
|
204
|
+
"CryptoAgent",
|
|
205
|
+
"NetworkAgent",
|
|
206
|
+
"PrivacyAgent",
|
|
207
|
+
"DependencyVulnerabilityAgent",
|
|
208
|
+
"ComplianceAgent",
|
|
209
|
+
]
|
|
210
|
+
|
|
211
|
+
crit_deductions = 0
|
|
212
|
+
high_deductions = 0
|
|
213
|
+
med_deductions = 0
|
|
214
|
+
|
|
215
|
+
for agent_name in security_agents:
|
|
216
|
+
agent_data = scans.get(agent_name, {})
|
|
217
|
+
for f in agent_data.get("findings", []):
|
|
218
|
+
sev = f.get("severity", "info")
|
|
219
|
+
if sev == "critical":
|
|
220
|
+
crit_deductions += 20
|
|
221
|
+
elif sev == "high":
|
|
222
|
+
high_deductions += 10
|
|
223
|
+
elif sev == "medium":
|
|
224
|
+
med_deductions += 3
|
|
225
|
+
|
|
226
|
+
score -= min(crit_deductions, 60)
|
|
227
|
+
score -= min(high_deductions, 30)
|
|
228
|
+
score -= med_deductions
|
|
229
|
+
|
|
230
|
+
return max(0.0, score)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _compute_test_coverage(brain: dict, scans: dict, real_pct: float = 0.0) -> float:
|
|
234
|
+
"""Test coverage score from TestScanner coverage_pct, with real % as fallback."""
|
|
235
|
+
test_data = scans.get("TestScanner", {})
|
|
236
|
+
pct = test_data.get("coverage_pct")
|
|
237
|
+
if pct is not None:
|
|
238
|
+
return float(pct)
|
|
239
|
+
if real_pct > 0:
|
|
240
|
+
return real_pct
|
|
241
|
+
return 50.0
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _compute_dead_code(brain: dict, scans: dict) -> float:
|
|
245
|
+
"""
|
|
246
|
+
Dead code score (0-100).
|
|
247
|
+
100 = no dead code. Deduction per dead file.
|
|
248
|
+
"""
|
|
249
|
+
dead_data = scans.get("DeadCodeScanner", {})
|
|
250
|
+
file_count = brain.get("file_count", 1) or 1
|
|
251
|
+
|
|
252
|
+
confirmed = len(dead_data.get("confirmed_dead", []))
|
|
253
|
+
broken = len(dead_data.get("broken_import", []))
|
|
254
|
+
total_dead = confirmed + (broken * 0.5)
|
|
255
|
+
|
|
256
|
+
dead_ratio = min(total_dead / file_count, 1.0)
|
|
257
|
+
score = 100 - (dead_ratio * 100)
|
|
258
|
+
return max(0.0, min(100.0, score))
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _compute_dependency(brain: dict, scans: dict) -> float:
|
|
262
|
+
"""
|
|
263
|
+
Dependency health: 100 = all clean. Score drops with ratio of vulnerable deps.
|
|
264
|
+
"""
|
|
265
|
+
dep_data = scans.get("DependencyScanner", {})
|
|
266
|
+
total = dep_data.get("total_deps", 0)
|
|
267
|
+
vulnerable = len(dep_data.get("vulnerable", []))
|
|
268
|
+
|
|
269
|
+
if total == 0:
|
|
270
|
+
return 80.0
|
|
271
|
+
|
|
272
|
+
ratio = vulnerable / total
|
|
273
|
+
score = 100 - (ratio * 150)
|
|
274
|
+
return max(0.0, min(100.0, score))
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def _compute_contract(brain: dict) -> float:
|
|
278
|
+
"""
|
|
279
|
+
Contract score: 100 = all flows confirmed. 0 = no contract established yet.
|
|
280
|
+
Partial: (confirmed / inferred) * 100
|
|
281
|
+
"""
|
|
282
|
+
inferred = brain.get("inferred_flows", [])
|
|
283
|
+
confirmed = brain.get("confirmed_flows", [])
|
|
284
|
+
|
|
285
|
+
if not inferred:
|
|
286
|
+
return 50.0
|
|
287
|
+
|
|
288
|
+
if not confirmed:
|
|
289
|
+
return 20.0
|
|
290
|
+
|
|
291
|
+
ratio = len(confirmed) / len(inferred)
|
|
292
|
+
return min(100.0, ratio * 100)
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
# ── Test coverage percentage ─────────────────────────────────────────────────
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _compute_test_coverage_pct(root: Path | None, brain: dict) -> float:
|
|
299
|
+
"""Count source files with test counterparts and return percentage.
|
|
300
|
+
|
|
301
|
+
Same semantics as the original implementation (a source file is covered
|
|
302
|
+
only by a test co-located in its own directory or a sibling tests/
|
|
303
|
+
directory) but implemented as set lookups instead of per-file stat
|
|
304
|
+
probes — that probe storm made this the dashboard's #1 bottleneck (~3.8s).
|
|
305
|
+
"""
|
|
306
|
+
if root is None:
|
|
307
|
+
return 0.0
|
|
308
|
+
|
|
309
|
+
file_count = brain.get("file_count", 0)
|
|
310
|
+
if file_count == 0:
|
|
311
|
+
return 0.0
|
|
312
|
+
|
|
313
|
+
source_extensions = {".py", ".js", ".ts", ".jsx", ".tsx", ".rb", ".go", ".rs", ".java"}
|
|
314
|
+
test_patterns = ("test_", "_test.", ".test.", ".spec.", "_spec.")
|
|
315
|
+
test_dirs = ("tests", "test", "__tests__", "spec")
|
|
316
|
+
|
|
317
|
+
try:
|
|
318
|
+
from patchi.core.agents.base import safe_rglob
|
|
319
|
+
|
|
320
|
+
# Pass 1 — walk the tree once, collecting existing test names and
|
|
321
|
+
# all source files. No per-file stat() calls.
|
|
322
|
+
src_by_dir: dict = {}
|
|
323
|
+
test_names_by_dir: dict = {}
|
|
324
|
+
for ext in source_extensions:
|
|
325
|
+
for f in safe_rglob(root, f"*{ext}"):
|
|
326
|
+
rel = f.relative_to(root).as_posix()
|
|
327
|
+
parts = Path(rel).parts
|
|
328
|
+
fname = f.name.lower()
|
|
329
|
+
|
|
330
|
+
in_test_dir = any(p.lower() in test_dirs for p in parts)
|
|
331
|
+
is_test_file = any(pat in fname for pat in test_patterns)
|
|
332
|
+
dir_key = str(f.parent)
|
|
333
|
+
|
|
334
|
+
if in_test_dir or is_test_file:
|
|
335
|
+
test_names_by_dir.setdefault(dir_key, set()).add(fname)
|
|
336
|
+
else:
|
|
337
|
+
src_by_dir.setdefault(dir_key, []).append(f)
|
|
338
|
+
|
|
339
|
+
source_files = 0
|
|
340
|
+
files_with_tests = 0
|
|
341
|
+
|
|
342
|
+
for dir_key, files in src_by_dir.items():
|
|
343
|
+
own_tests = test_names_by_dir.get(dir_key, set())
|
|
344
|
+
parent = Path(dir_key)
|
|
345
|
+
sibling_tests = set()
|
|
346
|
+
for td in test_dirs:
|
|
347
|
+
sibling_tests |= test_names_by_dir.get(str(parent / td), set())
|
|
348
|
+
|
|
349
|
+
for f in files:
|
|
350
|
+
source_files += 1
|
|
351
|
+
stem = f.stem
|
|
352
|
+
candidates = {
|
|
353
|
+
f"test_{stem}{ext}".lower()
|
|
354
|
+
for ext in source_extensions
|
|
355
|
+
} | {
|
|
356
|
+
f"{stem}_test{ext}".lower()
|
|
357
|
+
for ext in source_extensions
|
|
358
|
+
} | {
|
|
359
|
+
f"{stem}.test{ext}".lower()
|
|
360
|
+
for ext in source_extensions
|
|
361
|
+
} | {
|
|
362
|
+
f"{stem}.spec{ext}".lower()
|
|
363
|
+
for ext in source_extensions
|
|
364
|
+
}
|
|
365
|
+
if candidates & own_tests or candidates & sibling_tests:
|
|
366
|
+
files_with_tests += 1
|
|
367
|
+
|
|
368
|
+
if source_files == 0:
|
|
369
|
+
return 0.0
|
|
370
|
+
|
|
371
|
+
return round((files_with_tests / source_files) * 100, 1)
|
|
372
|
+
except Exception as e:
|
|
373
|
+
_log.warning("_compute_test_coverage_pct failed: %s", e)
|
|
374
|
+
return 0.0
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
# ── Grade + color ──────────────────────────────────────────────────────────────
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _grade(score: int) -> tuple[str, str]:
|
|
381
|
+
if score >= 90:
|
|
382
|
+
return "A", "#4ADE80"
|
|
383
|
+
if score >= 70:
|
|
384
|
+
return "B", "#86EFAC"
|
|
385
|
+
if score >= 50:
|
|
386
|
+
return "C", "#FACC15"
|
|
387
|
+
if score >= 30:
|
|
388
|
+
return "D", "#FB923C"
|
|
389
|
+
return "F", "#FF4D6D"
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
# ── Breakdown detail ───────────────────────────────────────────────────────────
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def _build_breakdown(
|
|
396
|
+
brain: dict, scans: dict, patches: list, test_coverage_pct: float = 0.0
|
|
397
|
+
) -> dict:
|
|
398
|
+
return {
|
|
399
|
+
"file_count": brain.get("file_count", 0),
|
|
400
|
+
"route_count": brain.get("route_count", 0),
|
|
401
|
+
"framework": brain.get("framework", "Unknown"),
|
|
402
|
+
"circular_deps": len(brain.get("circular_deps", [])),
|
|
403
|
+
"patches_applied": len(patches),
|
|
404
|
+
"last_scan": brain.get("last_scan", ""),
|
|
405
|
+
"agents_run": list(scans.keys()),
|
|
406
|
+
"test_coverage_pct": test_coverage_pct,
|
|
407
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Patchi hosted mode — live application monitoring and guard."""
|