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
patchi/core/scan_bus.py
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ScanBus §MASTER_ROADMAP Scan Bus — FileCorpus parse-once + shards + FindingBus.
|
|
3
|
+
|
|
4
|
+
Shards FileInfos for parallel agents without re-walking filesystem.
|
|
5
|
+
FindingBus is thread-safe queue for findings (replaces per-agent list merging).
|
|
6
|
+
QueueRunner batches agent classes across shard workers sharing one corpus.
|
|
7
|
+
|
|
8
|
+
File-sharding per agent activates only once agents honor `AgentInput.scope`
|
|
9
|
+
(4 do today: sast, security_config, unit_test, live_v2) — until then every
|
|
10
|
+
agent receives the full input and the bus is the single merge point.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import hashlib
|
|
16
|
+
import queue
|
|
17
|
+
import threading
|
|
18
|
+
from collections.abc import Callable
|
|
19
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
20
|
+
from dataclasses import dataclass, field
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
from patchi.core.brain.file_corpus import FileCorpus
|
|
25
|
+
from patchi.core.brain.scanner import FileInfo
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class ScanShard:
|
|
30
|
+
idx: int
|
|
31
|
+
files: list[FileInfo]
|
|
32
|
+
total_shards: int
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ScanBus:
|
|
36
|
+
"""Shard-aware FileCorpus wrapper. Build once, reuse across agents."""
|
|
37
|
+
|
|
38
|
+
def __init__(self, root: Path, shard_count: int = 4):
|
|
39
|
+
self.root = root
|
|
40
|
+
self.corpus = FileCorpus(root)
|
|
41
|
+
self.shard_count = max(1, shard_count)
|
|
42
|
+
self._shards: list[ScanShard] | None = None
|
|
43
|
+
|
|
44
|
+
def build(self) -> None:
|
|
45
|
+
# Force corpus build
|
|
46
|
+
_ = self.corpus.entries
|
|
47
|
+
|
|
48
|
+
def shards(self, file_infos: list[FileInfo] | None = None) -> list[ScanShard]:
|
|
49
|
+
if file_infos is None:
|
|
50
|
+
# Build from corpus if available, else scan
|
|
51
|
+
from patchi.core.brain.scanner import FileScanner
|
|
52
|
+
file_infos = FileScanner(self.root, corpus=self.corpus).scan()
|
|
53
|
+
n = self.shard_count
|
|
54
|
+
# shard by hash of path for even distribution
|
|
55
|
+
buckets: list[list[FileInfo]] = [[] for _ in range(n)]
|
|
56
|
+
# Deterministic sharding via MD5 (Python's hash() is randomized across processes)
|
|
57
|
+
for fi in file_infos:
|
|
58
|
+
digest = hashlib.md5(fi.path.encode()).digest()
|
|
59
|
+
shard_id = int.from_bytes(digest[:4], "little") % n
|
|
60
|
+
buckets[shard_id].append(fi)
|
|
61
|
+
return [ScanShard(idx=i, files=buckets[i], total_shards=n) for i in range(n)]
|
|
62
|
+
|
|
63
|
+
def shard_paths(self, file_infos: list[FileInfo] | None = None) -> list[list[str]]:
|
|
64
|
+
"""Relative path lists per shard — future per-agent scope input."""
|
|
65
|
+
return [[fi.path for fi in s.files] for s in self.shards(file_infos)]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class FindingBus:
|
|
70
|
+
"""Thread-safe bus for findings. Agents push, coordinator drains."""
|
|
71
|
+
|
|
72
|
+
_q: queue.Queue = field(default_factory=queue.Queue)
|
|
73
|
+
_lock: threading.Lock = field(default_factory=threading.Lock)
|
|
74
|
+
_count: int = 0
|
|
75
|
+
|
|
76
|
+
def publish(self, finding: Any) -> None:
|
|
77
|
+
self._q.put(finding)
|
|
78
|
+
with self._lock:
|
|
79
|
+
self._count += 1
|
|
80
|
+
|
|
81
|
+
def drain(self) -> list[Any]:
|
|
82
|
+
out = []
|
|
83
|
+
while not self._q.empty():
|
|
84
|
+
try:
|
|
85
|
+
out.append(self._q.get_nowait())
|
|
86
|
+
except queue.Empty:
|
|
87
|
+
break
|
|
88
|
+
return out
|
|
89
|
+
|
|
90
|
+
@property
|
|
91
|
+
def count(self) -> int:
|
|
92
|
+
with self._lock:
|
|
93
|
+
return self._count
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class QueueRunner:
|
|
97
|
+
"""Shard-worked parallel runner over agent classes.
|
|
98
|
+
|
|
99
|
+
One ScanBus (single FileCorpus build) is shared by all workers. Agent
|
|
100
|
+
classes are fanned out over `shard_count` threads with the same per-agent
|
|
101
|
+
timeout semantics as the coordinator's thread pool. Every finished
|
|
102
|
+
result's findings are auto-published to the FindingBus — agents never
|
|
103
|
+
touch the bus directly — so `drain()` is the single merge point.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
def __init__(self, bus: ScanBus, finding_bus: FindingBus | None = None):
|
|
107
|
+
self.scan_bus = bus
|
|
108
|
+
self.finding_bus = finding_bus or FindingBus()
|
|
109
|
+
self.scan_bus.build()
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def shards(self) -> list[ScanShard]:
|
|
113
|
+
return self.scan_bus.shards()
|
|
114
|
+
|
|
115
|
+
def run(
|
|
116
|
+
self,
|
|
117
|
+
agent_classes: list,
|
|
118
|
+
run_one: Callable[[Any], Any],
|
|
119
|
+
on_done: Callable[[Any], None],
|
|
120
|
+
default_timeout: int = 120,
|
|
121
|
+
) -> list:
|
|
122
|
+
"""Run every agent class, publish findings, call on_done per result."""
|
|
123
|
+
from concurrent.futures import Future
|
|
124
|
+
|
|
125
|
+
from patchi.core.agents.base import AgentGroup, AgentResult, AgentStatus
|
|
126
|
+
|
|
127
|
+
results: list = []
|
|
128
|
+
futures: dict[Future, str] = {}
|
|
129
|
+
timeout_map: dict[Future, int] = {}
|
|
130
|
+
with ThreadPoolExecutor(max_workers=self.scan_bus.shard_count) as ex:
|
|
131
|
+
for cls in agent_classes:
|
|
132
|
+
fut = ex.submit(self._run_and_publish, run_one, cls)
|
|
133
|
+
futures[fut] = getattr(cls, "name", str(cls))
|
|
134
|
+
timeout_map[fut] = getattr(cls, "timeout", default_timeout)
|
|
135
|
+
for fut in as_completed(futures):
|
|
136
|
+
try:
|
|
137
|
+
result = fut.result(timeout=timeout_map.get(fut, default_timeout))
|
|
138
|
+
except Exception as e:
|
|
139
|
+
result = AgentResult(
|
|
140
|
+
agent_name=futures[fut],
|
|
141
|
+
agent_group=AgentGroup.SCANNER,
|
|
142
|
+
status=AgentStatus.FAILED,
|
|
143
|
+
errors=[f"QueueRunner caught: {e}"],
|
|
144
|
+
)
|
|
145
|
+
on_done(result)
|
|
146
|
+
results.append(result)
|
|
147
|
+
return results
|
|
148
|
+
|
|
149
|
+
def _run_and_publish(self, run_one: Callable[[Any], Any], cls: Any) -> Any:
|
|
150
|
+
result = run_one(cls)
|
|
151
|
+
for f in getattr(result, "findings", []):
|
|
152
|
+
self.finding_bus.publish(f)
|
|
153
|
+
return result
|
|
154
|
+
|
|
155
|
+
def drain(self) -> list[Any]:
|
|
156
|
+
return self.finding_bus.drain()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AI Validator — the core harness engine that eliminates false positives.
|
|
3
|
+
|
|
4
|
+
Instead of asking "is this a vulnerability?" (which produces noise), the
|
|
5
|
+
validator asks three chained questions:
|
|
6
|
+
|
|
7
|
+
1. CODE READING: "Read the surrounding function. What does this code actually do?"
|
|
8
|
+
2. DATA FLOW: "Where does the tainted input come from and where does it go?"
|
|
9
|
+
3. EXPLOITABILITY: "Given the app context, is an attacker able to reach this?"
|
|
10
|
+
|
|
11
|
+
Each question builds on the previous answer, creating a reasoning chain that
|
|
12
|
+
produces a structured verdict with confidence and evidence.
|
|
13
|
+
|
|
14
|
+
The output is deterministic enough for the ConfidenceGate to score, but rich
|
|
15
|
+
enough for human review when needed.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import hashlib
|
|
21
|
+
import logging
|
|
22
|
+
from dataclasses import dataclass, field
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
from typing import Any
|
|
25
|
+
|
|
26
|
+
_log = logging.getLogger("patchi.security.ai_validator")
|
|
27
|
+
|
|
28
|
+
# ── Result types ─────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class ReasoningStep:
|
|
33
|
+
"""One step in the chain-of-reasoning."""
|
|
34
|
+
|
|
35
|
+
question: str
|
|
36
|
+
analysis: str
|
|
37
|
+
confidence: float # 0.0 - 1.0
|
|
38
|
+
|
|
39
|
+
def to_dict(self) -> dict:
|
|
40
|
+
return {
|
|
41
|
+
"question": self.question,
|
|
42
|
+
"analysis": self.analysis,
|
|
43
|
+
"confidence": self.confidence,
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@dataclass
|
|
48
|
+
class ValidationResult:
|
|
49
|
+
"""Full validation result with chain-of-reasoning."""
|
|
50
|
+
|
|
51
|
+
finding_id: str
|
|
52
|
+
is_true_positive: bool
|
|
53
|
+
confidence: float # 0.0 - 1.0
|
|
54
|
+
reasoning: list[ReasoningStep] = field(default_factory=list)
|
|
55
|
+
explanation: str = ""
|
|
56
|
+
fix_suggestion: str = ""
|
|
57
|
+
test_suggestion: str = ""
|
|
58
|
+
severity_adjustment: str = "" # "increase", "decrease", ""
|
|
59
|
+
tokens_used: int = 0
|
|
60
|
+
model: str = ""
|
|
61
|
+
|
|
62
|
+
@property
|
|
63
|
+
def summary(self) -> str:
|
|
64
|
+
verdict = "TRUE POSITIVE" if self.is_true_positive else "FALSE POSITIVE"
|
|
65
|
+
return f"{verdict} (confidence {self.confidence:.0%})"
|
|
66
|
+
|
|
67
|
+
def to_dict(self) -> dict:
|
|
68
|
+
return {
|
|
69
|
+
"finding_id": self.finding_id,
|
|
70
|
+
"is_true_positive": self.is_true_positive,
|
|
71
|
+
"confidence": self.confidence,
|
|
72
|
+
"reasoning": [s.to_dict() for s in self.reasoning],
|
|
73
|
+
"explanation": self.explanation,
|
|
74
|
+
"fix_suggestion": self.fix_suggestion,
|
|
75
|
+
"test_suggestion": self.test_suggestion,
|
|
76
|
+
"severity_adjustment": self.severity_adjustment,
|
|
77
|
+
"tokens_used": self.tokens_used,
|
|
78
|
+
"model": self.model,
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
# ── Heuristic pre-filters (no AI needed) ─────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
_FP_PATTERNS: list[tuple[str, str, float]] = [
|
|
85
|
+
# (file pattern, finding type, confidence_penalty)
|
|
86
|
+
(".lock", "dependency", 0.8),
|
|
87
|
+
("package-lock.json", "dependency", 0.9),
|
|
88
|
+
("yarn.lock", "dependency", 0.9),
|
|
89
|
+
("Pipfile.lock", "dependency", 0.9),
|
|
90
|
+
(".min.js", "code_quality", 0.7),
|
|
91
|
+
(".min.css", "code_quality", 0.7),
|
|
92
|
+
("__pycache__", "code_quality", 0.9),
|
|
93
|
+
(".generated.", "code_quality", 0.8),
|
|
94
|
+
(".pb.", "code_quality", 0.7),
|
|
95
|
+
("_pb2.py", "code_quality", 0.8),
|
|
96
|
+
(".proto", "code_quality", 0.6),
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
_TEST_PATTERNS = [
|
|
100
|
+
"test_",
|
|
101
|
+
"_test.",
|
|
102
|
+
"spec_",
|
|
103
|
+
"_spec.",
|
|
104
|
+
"tests/",
|
|
105
|
+
"test/",
|
|
106
|
+
"__tests__/",
|
|
107
|
+
"fixture",
|
|
108
|
+
"mock_",
|
|
109
|
+
"fake_",
|
|
110
|
+
"conftest",
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
_GENERATED_MARKERS = [
|
|
114
|
+
"generated by",
|
|
115
|
+
"auto-generated",
|
|
116
|
+
"auto generated",
|
|
117
|
+
"do not edit",
|
|
118
|
+
"do not modify",
|
|
119
|
+
"autogenerated",
|
|
120
|
+
"@generated",
|
|
121
|
+
"# noqa",
|
|
122
|
+
"// generated",
|
|
123
|
+
"code-generated",
|
|
124
|
+
"machine-generated",
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def heuristic_pre_filter(finding: Any) -> tuple[str, float] | None:
|
|
129
|
+
"""Quick heuristic check — returns (reason, penalty) if likely FP, None otherwise.
|
|
130
|
+
|
|
131
|
+
This runs BEFORE the AI validator to skip obviously non-exploitable
|
|
132
|
+
findings, saving tokens and improving signal.
|
|
133
|
+
"""
|
|
134
|
+
ffile = getattr(finding, "file", "") or ""
|
|
135
|
+
ftype = getattr(finding, "type", "") or ""
|
|
136
|
+
snippet = getattr(finding, "code_snippet", "") or ""
|
|
137
|
+
|
|
138
|
+
# 1. File pattern FP
|
|
139
|
+
ffile_lower = ffile.lower()
|
|
140
|
+
for pattern, ftype_match, penalty in _FP_PATTERNS:
|
|
141
|
+
if pattern in ffile_lower and (not ftype_match or ftype_match in ftype.lower()):
|
|
142
|
+
return f"File pattern match: {pattern}", penalty
|
|
143
|
+
|
|
144
|
+
# 2. Test file FP
|
|
145
|
+
if any(tp in ffile_lower for tp in _TEST_PATTERNS):
|
|
146
|
+
# Test files with injection findings are often intentional test payloads
|
|
147
|
+
if any(kw in ftype.lower() for kw in ["injection", "xss", "command", "traversal"]):
|
|
148
|
+
return "Test file with injection pattern (likely test payload)", 0.6
|
|
149
|
+
|
|
150
|
+
# 3. Generated code FP
|
|
151
|
+
snippet_lower = snippet.lower()
|
|
152
|
+
if any(marker in snippet_lower for marker in _GENERATED_MARKERS):
|
|
153
|
+
return "Generated code marker detected", 0.7
|
|
154
|
+
|
|
155
|
+
# 4. TODO/FIXME with security mention = not a real vuln yet
|
|
156
|
+
if "todo" in snippet_lower and any(
|
|
157
|
+
kw in ftype.lower() for kw in ["security", "auth", "crypto"]
|
|
158
|
+
):
|
|
159
|
+
return "TODO/FIXME marker — not implemented yet", 0.5
|
|
160
|
+
|
|
161
|
+
return None
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
# ── AI Validator ──────────────────────────────────────────────────────────────
|
|
165
|
+
|
|
166
|
+
_SYSTEM_PROMPT = """\
|
|
167
|
+
You are Patchi's security harness validator. You receive a security finding with its full file context and must determine if it is a TRUE POSITIVE or FALSE POSITIVE.
|
|
168
|
+
|
|
169
|
+
You must produce a chain-of-reasoning in 3 steps:
|
|
170
|
+
|
|
171
|
+
STEP 1 — CODE READING:
|
|
172
|
+
Read the function/method containing the finding. What does the code actually do? Is the pattern suspicious or benign in context?
|
|
173
|
+
|
|
174
|
+
STEP 2 — DATA FLOW:
|
|
175
|
+
Trace the data flow. Where does user-controlled input come from? Is there any sanitization between source and sink? Is the path realistic for an attacker?
|
|
176
|
+
|
|
177
|
+
STEP 3 — EXPLOITABILITY:
|
|
178
|
+
Given the application context (routes, middleware, auth), can an attacker actually reach this code? What is the real-world impact?
|
|
179
|
+
|
|
180
|
+
After the 3 steps, output a structured JSON verdict:
|
|
181
|
+
{
|
|
182
|
+
"is_true_positive": bool,
|
|
183
|
+
"confidence": float (0.0-1.0),
|
|
184
|
+
"reasoning": [
|
|
185
|
+
{"question": "...", "analysis": "...", "confidence": float},
|
|
186
|
+
{"question": "...", "analysis": "...", "confidence": float},
|
|
187
|
+
{"question": "...", "analysis": "...", "confidence": float}
|
|
188
|
+
],
|
|
189
|
+
"explanation": "one-sentence verdict summary",
|
|
190
|
+
"fix_suggestion": "concrete fix code or empty string",
|
|
191
|
+
"test_suggestion": "test case to verify fix or empty string",
|
|
192
|
+
"severity_adjustment": "increase" | "decrease" | ""
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
Rules:
|
|
196
|
+
- Be CONSERVATIVE: if you are not sure, mark as true positive with lower confidence
|
|
197
|
+
- Never dismiss a real vulnerability to reduce noise
|
|
198
|
+
- Always explain WHY it is/isn't exploitable
|
|
199
|
+
- Generate minimal, targeted fixes (not architectural rewrites)
|
|
200
|
+
- The fix must preserve existing functionality
|
|
201
|
+
"""
|
|
202
|
+
|
|
203
|
+
_CACHE_TTL = 86400 # 24h
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
class AIValidator:
|
|
207
|
+
"""AI-powered true/false positive validator with chain-of-reasoning.
|
|
208
|
+
|
|
209
|
+
Usage::
|
|
210
|
+
|
|
211
|
+
validator = AIValidator(root)
|
|
212
|
+
result = validator.validate(finding, file_content="...")
|
|
213
|
+
if result.confidence >= 0.8 and not result.is_true_positive:
|
|
214
|
+
# High-confidence false positive — safe to discard
|
|
215
|
+
pass
|
|
216
|
+
"""
|
|
217
|
+
|
|
218
|
+
def __init__(self, root: Path, config: dict | None = None):
|
|
219
|
+
self.root = root
|
|
220
|
+
self.config = config or {}
|
|
221
|
+
self._cache: dict[str, tuple[float, ValidationResult]] = {}
|
|
222
|
+
self._stats = {"validated": 0, "cached": 0, "heuristic_skip": 0}
|
|
223
|
+
|
|
224
|
+
def validate(
|
|
225
|
+
self,
|
|
226
|
+
finding: Any,
|
|
227
|
+
file_content: str | None = None,
|
|
228
|
+
route_context: dict | None = None,
|
|
229
|
+
use_heuristics: bool = True,
|
|
230
|
+
) -> ValidationResult:
|
|
231
|
+
"""Validate a single finding.
|
|
232
|
+
|
|
233
|
+
Args:
|
|
234
|
+
finding: A Finding or GatedFinding with .file, .line, .type, .message,
|
|
235
|
+
.code_snippet, .severity, .cwe attributes.
|
|
236
|
+
file_content: Full source file content (read if not provided).
|
|
237
|
+
route_context: Dict with 'routes', 'middleware', 'auth' for context.
|
|
238
|
+
use_heuristics: Run heuristic pre-filter before AI.
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
ValidationResult with chain-of-reasoning.
|
|
242
|
+
"""
|
|
243
|
+
fid = self._finding_id(finding)
|
|
244
|
+
self._stats["validated"] += 1
|
|
245
|
+
|
|
246
|
+
# Check cache
|
|
247
|
+
if fid in self._cache:
|
|
248
|
+
ts, cached = self._cache[fid]
|
|
249
|
+
import time
|
|
250
|
+
|
|
251
|
+
if time.time() - ts < _CACHE_TTL:
|
|
252
|
+
self._stats["cached"] += 1
|
|
253
|
+
return cached
|
|
254
|
+
|
|
255
|
+
# Heuristic pre-filter
|
|
256
|
+
if use_heuristics:
|
|
257
|
+
hp = heuristic_pre_filter(finding)
|
|
258
|
+
if hp:
|
|
259
|
+
reason, penalty = hp
|
|
260
|
+
sev = getattr(finding, "severity", None)
|
|
261
|
+
sev_val = sev.value if hasattr(sev, "value") else str(sev)
|
|
262
|
+
# Only auto-discard if severity is low AND penalty is high
|
|
263
|
+
if penalty >= 0.7 and sev_val in ("info", "low"):
|
|
264
|
+
result = ValidationResult(
|
|
265
|
+
finding_id=fid,
|
|
266
|
+
is_true_positive=False,
|
|
267
|
+
confidence=max(0.1, 1.0 - penalty),
|
|
268
|
+
explanation=f"Heuristic filter: {reason}",
|
|
269
|
+
)
|
|
270
|
+
self._cache[fid] = (self._now(), result)
|
|
271
|
+
self._stats["heuristic_skip"] += 1
|
|
272
|
+
return result
|
|
273
|
+
|
|
274
|
+
# Read file content if not provided
|
|
275
|
+
if file_content is None:
|
|
276
|
+
file_content = self._read_file(getattr(finding, "file", ""))
|
|
277
|
+
|
|
278
|
+
# Build the 3-step prompt
|
|
279
|
+
prompt = self._build_prompt(finding, file_content, route_context)
|
|
280
|
+
|
|
281
|
+
# Call AI
|
|
282
|
+
result = self._call_ai(fid, prompt)
|
|
283
|
+
self._cache[fid] = (self._now(), result)
|
|
284
|
+
return result
|
|
285
|
+
|
|
286
|
+
def validate_batch(
|
|
287
|
+
self,
|
|
288
|
+
findings: list,
|
|
289
|
+
use_heuristics: bool = True,
|
|
290
|
+
) -> list[ValidationResult]:
|
|
291
|
+
"""Validate a batch of findings, using cache and heuristics."""
|
|
292
|
+
return [self.validate(f, use_heuristics=use_heuristics) for f in findings]
|
|
293
|
+
|
|
294
|
+
def get_stats(self) -> dict:
|
|
295
|
+
return dict(self._stats)
|
|
296
|
+
|
|
297
|
+
# ── Internal ──────────────────────────────────────────────────────────────
|
|
298
|
+
|
|
299
|
+
def _finding_id(self, finding: Any) -> str:
|
|
300
|
+
raw = (
|
|
301
|
+
f"{getattr(finding, 'file', '')}:"
|
|
302
|
+
f"{getattr(finding, 'line', 0)}:"
|
|
303
|
+
f"{getattr(finding, 'type', '')}:"
|
|
304
|
+
f"{getattr(finding, 'message', '')[:80]}"
|
|
305
|
+
)
|
|
306
|
+
return hashlib.sha256(raw.encode()).hexdigest()[:16]
|
|
307
|
+
|
|
308
|
+
def _read_file(self, filepath: str) -> str:
|
|
309
|
+
if not filepath:
|
|
310
|
+
return ""
|
|
311
|
+
try:
|
|
312
|
+
full = self.root / filepath
|
|
313
|
+
if full.is_file():
|
|
314
|
+
return full.read_text(encoding="utf-8", errors="replace")[:8000]
|
|
315
|
+
except Exception as _exc:
|
|
316
|
+
_log.warning('_read_file failed: %s', _exc)
|
|
317
|
+
return ""
|
|
318
|
+
|
|
319
|
+
def _build_prompt(
|
|
320
|
+
self,
|
|
321
|
+
finding: Any,
|
|
322
|
+
file_content: str,
|
|
323
|
+
route_context: dict | None,
|
|
324
|
+
) -> str:
|
|
325
|
+
ffile = getattr(finding, "file", "")
|
|
326
|
+
fline = getattr(finding, "line", 0)
|
|
327
|
+
ftype = getattr(finding, "type", "")
|
|
328
|
+
message = getattr(finding, "message", "")
|
|
329
|
+
snippet = getattr(finding, "code_snippet", "")
|
|
330
|
+
severity = getattr(finding, "severity", "")
|
|
331
|
+
sev_val = severity.value if hasattr(severity, "value") else str(severity)
|
|
332
|
+
cwe = getattr(finding, "cwe", "")
|
|
333
|
+
confirmed_by = getattr(finding, "confirmed_by", [])
|
|
334
|
+
|
|
335
|
+
# Extract the function around the finding line
|
|
336
|
+
context_block = self._extract_context_block(file_content, fline)
|
|
337
|
+
|
|
338
|
+
parts = [
|
|
339
|
+
f"FINDING: {ftype} at {ffile}:{fline}",
|
|
340
|
+
f"Severity: {sev_val}",
|
|
341
|
+
f"CWE: {cwe}" if cwe else "",
|
|
342
|
+
f"Message: {message}",
|
|
343
|
+
f"Confirmed by: {', '.join(confirmed_by) if confirmed_by else 'N/A'}",
|
|
344
|
+
"",
|
|
345
|
+
"--- CODE SNIPPET (from finding) ---",
|
|
346
|
+
snippet if snippet else "(no snippet available)",
|
|
347
|
+
"",
|
|
348
|
+
f"--- SURROUNDING FUNCTION (lines {max(1, fline - 20)}-{fline + 20}) ---",
|
|
349
|
+
context_block if context_block else "(could not extract context)",
|
|
350
|
+
]
|
|
351
|
+
|
|
352
|
+
if route_context:
|
|
353
|
+
routes = route_context.get("routes", [])
|
|
354
|
+
auth = route_context.get("auth", "")
|
|
355
|
+
if routes:
|
|
356
|
+
parts.append(f"\n--- ROUTE CONTEXT ---\nAuth: {auth}")
|
|
357
|
+
for r in routes[:5]:
|
|
358
|
+
if isinstance(r, dict):
|
|
359
|
+
parts.append(f" {r.get('method', '?')} {r.get('path', '?')}")
|
|
360
|
+
|
|
361
|
+
parts.append("\nAnalyze this finding through the 3-step reasoning chain.")
|
|
362
|
+
return "\n".join(p for p in parts if p)
|
|
363
|
+
|
|
364
|
+
def _extract_context_block(self, file_content: str, line: int) -> str:
|
|
365
|
+
"""Extract the function/block surrounding the finding line."""
|
|
366
|
+
if not file_content or line <= 0:
|
|
367
|
+
return ""
|
|
368
|
+
lines = file_content.splitlines()
|
|
369
|
+
if line > len(lines):
|
|
370
|
+
return ""
|
|
371
|
+
|
|
372
|
+
# Find function start (go up to find `def` or `function` or `class`)
|
|
373
|
+
start = max(0, line - 30)
|
|
374
|
+
for i in range(line - 1, max(0, line - 40), -1):
|
|
375
|
+
line_text = lines[i].strip()
|
|
376
|
+
if (
|
|
377
|
+
line_text.startswith("def ")
|
|
378
|
+
or line_text.startswith("function ")
|
|
379
|
+
or line_text.startswith("class ")
|
|
380
|
+
):
|
|
381
|
+
start = i
|
|
382
|
+
break
|
|
383
|
+
if line_text.startswith("@") and i + 1 < len(lines):
|
|
384
|
+
start = i
|
|
385
|
+
break
|
|
386
|
+
|
|
387
|
+
# Find function end (go down to find next def/class at same indent)
|
|
388
|
+
end = min(len(lines), line + 30)
|
|
389
|
+
base_indent = (
|
|
390
|
+
len(lines[line - 1]) - len(lines[line - 1].lstrip()) if line <= len(lines) else 0
|
|
391
|
+
)
|
|
392
|
+
for i in range(line, min(len(lines), line + 50)):
|
|
393
|
+
cur_line = lines[i]
|
|
394
|
+
if cur_line.strip() and not cur_line.strip().startswith("#"):
|
|
395
|
+
indent = len(cur_line) - len(cur_line.lstrip())
|
|
396
|
+
if indent <= base_indent and (
|
|
397
|
+
cur_line.strip().startswith("def ")
|
|
398
|
+
or cur_line.strip().startswith("class ")
|
|
399
|
+
or cur_line.strip().startswith("async def ")
|
|
400
|
+
):
|
|
401
|
+
end = i
|
|
402
|
+
break
|
|
403
|
+
|
|
404
|
+
# Format with line numbers
|
|
405
|
+
block_lines = lines[start:end]
|
|
406
|
+
formatted = []
|
|
407
|
+
for i, line_text in enumerate(block_lines, start=start + 1):
|
|
408
|
+
marker = " >>>" if i == line else " "
|
|
409
|
+
formatted.append(f"{i:4d}{marker} {line_text}")
|
|
410
|
+
return "\n".join(formatted)
|
|
411
|
+
|
|
412
|
+
def _call_ai(self, fid: str, prompt: str) -> ValidationResult:
|
|
413
|
+
"""Make the AI call and parse the structured response."""
|
|
414
|
+
try:
|
|
415
|
+
from patchi.core.ai.client import call_ai_structured
|
|
416
|
+
|
|
417
|
+
response = call_ai_structured(
|
|
418
|
+
prompt=prompt,
|
|
419
|
+
system=_SYSTEM_PROMPT,
|
|
420
|
+
output_model=dict,
|
|
421
|
+
)
|
|
422
|
+
|
|
423
|
+
if response and isinstance(response, dict):
|
|
424
|
+
reasoning_steps = []
|
|
425
|
+
for step in response.get("reasoning", []):
|
|
426
|
+
if isinstance(step, dict):
|
|
427
|
+
reasoning_steps.append(
|
|
428
|
+
ReasoningStep(
|
|
429
|
+
question=step.get("question", ""),
|
|
430
|
+
analysis=step.get("analysis", ""),
|
|
431
|
+
confidence=float(step.get("confidence", 0.5)),
|
|
432
|
+
)
|
|
433
|
+
)
|
|
434
|
+
|
|
435
|
+
return ValidationResult(
|
|
436
|
+
finding_id=fid,
|
|
437
|
+
is_true_positive=bool(response.get("is_true_positive", True)),
|
|
438
|
+
confidence=float(response.get("confidence", 0.5)),
|
|
439
|
+
reasoning=reasoning_steps,
|
|
440
|
+
explanation=response.get("explanation", ""),
|
|
441
|
+
fix_suggestion=response.get("fix_suggestion", ""),
|
|
442
|
+
test_suggestion=response.get("test_suggestion", ""),
|
|
443
|
+
severity_adjustment=response.get("severity_adjustment", ""),
|
|
444
|
+
tokens_used=int(response.get("tokens_used", 0)),
|
|
445
|
+
model=response.get("model", "unknown"),
|
|
446
|
+
)
|
|
447
|
+
except Exception as e:
|
|
448
|
+
_log.debug("AIValidator._call_ai failed: %s", e)
|
|
449
|
+
|
|
450
|
+
# Offline fallback — assume true positive with low confidence
|
|
451
|
+
return ValidationResult(
|
|
452
|
+
finding_id=fid,
|
|
453
|
+
is_true_positive=True,
|
|
454
|
+
confidence=0.3,
|
|
455
|
+
explanation="AI unavailable — assumed true positive for safety",
|
|
456
|
+
)
|
|
457
|
+
|
|
458
|
+
@staticmethod
|
|
459
|
+
def _now() -> float:
|
|
460
|
+
import time
|
|
461
|
+
|
|
462
|
+
return time.time()
|