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,153 @@
|
|
|
1
|
+
"""DataFlowAbuseCampaign — test data flow security.
|
|
2
|
+
|
|
3
|
+
Maps data flows, tests sanitization, encoding bypass, cross-boundary
|
|
4
|
+
flows, and security meaning changes.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
from .base import Campaign, CampaignStep
|
|
12
|
+
|
|
13
|
+
_log = logging.getLogger("patchi.core.campaigns.data_flow_abuse")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DataFlowAbuseCampaign(Campaign):
|
|
17
|
+
"""Test data flow injection and security meaning."""
|
|
18
|
+
|
|
19
|
+
name = "data_flow_abuse"
|
|
20
|
+
description = "Tests data flow injection and security"
|
|
21
|
+
|
|
22
|
+
def steps(self) -> list[CampaignStep]:
|
|
23
|
+
return [
|
|
24
|
+
CampaignStep(name="map_flows", description="Map all data flows from the graph"),
|
|
25
|
+
CampaignStep(
|
|
26
|
+
name="identify_transformations",
|
|
27
|
+
description="Identify data transformations in flows",
|
|
28
|
+
),
|
|
29
|
+
CampaignStep(
|
|
30
|
+
name="test_sanitization", description="Test sanitization on sensitive flows"
|
|
31
|
+
),
|
|
32
|
+
CampaignStep(
|
|
33
|
+
name="test_encoding_bypass", description="Test encoding bypass on transformations"
|
|
34
|
+
),
|
|
35
|
+
CampaignStep(name="test_cross_boundary", description="Test cross-boundary data flows"),
|
|
36
|
+
CampaignStep(name="test_security_meaning", description="Test security meaning changes"),
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
def _execute_step(self, step: CampaignStep) -> None:
|
|
40
|
+
if step.name == "map_flows":
|
|
41
|
+
self._map_flows(step)
|
|
42
|
+
elif step.name == "identify_transformations":
|
|
43
|
+
self._identify_transformations(step)
|
|
44
|
+
elif step.name == "test_sanitization":
|
|
45
|
+
self._test_sanitization(step)
|
|
46
|
+
elif step.name == "test_encoding_bypass":
|
|
47
|
+
self._test_encoding_bypass(step)
|
|
48
|
+
elif step.name == "test_cross_boundary":
|
|
49
|
+
self._test_cross_boundary(step)
|
|
50
|
+
elif step.name == "test_security_meaning":
|
|
51
|
+
self._test_security_meaning(step)
|
|
52
|
+
|
|
53
|
+
def _map_flows(self, step: CampaignStep) -> None:
|
|
54
|
+
"""Map all data flows mentioned in the graph."""
|
|
55
|
+
flows: list[dict] = []
|
|
56
|
+
for claim in self._graph.claims.values():
|
|
57
|
+
for ev in claim.evidence:
|
|
58
|
+
a = ev.artifact
|
|
59
|
+
if "source" in a and "sink" in a:
|
|
60
|
+
flows.append(
|
|
61
|
+
{
|
|
62
|
+
"source": a["source"],
|
|
63
|
+
"sink": a["sink"],
|
|
64
|
+
"sanitized": a.get("sanitized", False),
|
|
65
|
+
"cross_boundary": a.get("cross_boundary", False),
|
|
66
|
+
}
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
if not flows:
|
|
70
|
+
step.findings.append(
|
|
71
|
+
{
|
|
72
|
+
"type": "no_flows_found",
|
|
73
|
+
"severity": "info",
|
|
74
|
+
"detail": "No data flows discovered from the assurance graph",
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
else:
|
|
78
|
+
step.findings.append(
|
|
79
|
+
{
|
|
80
|
+
"type": "flows_mapped",
|
|
81
|
+
"severity": "info",
|
|
82
|
+
"detail": f"Found {len(flows)} data flows",
|
|
83
|
+
}
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def _identify_transformations(self, step: CampaignStep) -> None:
|
|
87
|
+
"""Check for unsanitized transformations in data flows."""
|
|
88
|
+
for claim in self._graph.claims.values():
|
|
89
|
+
for ev in claim.evidence:
|
|
90
|
+
a = ev.artifact
|
|
91
|
+
if "source" in a and "sink" in a:
|
|
92
|
+
if not a.get("sanitized", True):
|
|
93
|
+
step.findings.append(
|
|
94
|
+
{
|
|
95
|
+
"type": "unsanitized_flow",
|
|
96
|
+
"severity": "high",
|
|
97
|
+
"detail": f"Unsanitized flow: {a['source']} → {a['sink']}",
|
|
98
|
+
}
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
def _test_sanitization(self, step: CampaignStep) -> None:
|
|
102
|
+
"""Test that sensitive data flows are properly sanitized."""
|
|
103
|
+
for claim in self._graph.claims.values():
|
|
104
|
+
if "sanitiz" in claim.statement.lower():
|
|
105
|
+
if claim.verdict.value == "disproved":
|
|
106
|
+
step.findings.append(
|
|
107
|
+
{
|
|
108
|
+
"type": "sanitization_bypass",
|
|
109
|
+
"severity": "critical",
|
|
110
|
+
"detail": f"Sanitization bypassed: {claim.statement}",
|
|
111
|
+
}
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
def _test_encoding_bypass(self, step: CampaignStep) -> None:
|
|
115
|
+
"""Test for encoding bypass on data transformations."""
|
|
116
|
+
for claim in self._graph.claims.values():
|
|
117
|
+
if "encod" in claim.statement.lower() or "transform" in claim.statement.lower():
|
|
118
|
+
if claim.verdict.value == "disproved":
|
|
119
|
+
step.findings.append(
|
|
120
|
+
{
|
|
121
|
+
"type": "encoding_bypass",
|
|
122
|
+
"severity": "high",
|
|
123
|
+
"detail": f"Encoding bypass found: {claim.statement}",
|
|
124
|
+
}
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
def _test_cross_boundary(self, step: CampaignStep) -> None:
|
|
128
|
+
"""Test that cross-boundary flows have proper controls."""
|
|
129
|
+
for claim in self._graph.claims.values():
|
|
130
|
+
for ev in claim.evidence:
|
|
131
|
+
a = ev.artifact
|
|
132
|
+
if a.get("cross_boundary", False):
|
|
133
|
+
if ev.supports: # evidence supports that it's uncontrolled
|
|
134
|
+
step.findings.append(
|
|
135
|
+
{
|
|
136
|
+
"type": "uncontrolled_cross_boundary",
|
|
137
|
+
"severity": "critical",
|
|
138
|
+
"detail": f"Cross-boundary flow without controls: {a.get('source', '?')} → {a.get('sink', '?')}",
|
|
139
|
+
}
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
def _test_security_meaning(self, step: CampaignStep) -> None:
|
|
143
|
+
"""Test that data flow transformations don't change security meaning."""
|
|
144
|
+
for claim in self._graph.claims.values():
|
|
145
|
+
if "meaning" in claim.statement.lower() and "security" in claim.statement.lower():
|
|
146
|
+
if claim.verdict.value == "disproved":
|
|
147
|
+
step.findings.append(
|
|
148
|
+
{
|
|
149
|
+
"type": "security_meaning_change",
|
|
150
|
+
"severity": "high",
|
|
151
|
+
"detail": f"Security meaning changed: {claim.statement}",
|
|
152
|
+
}
|
|
153
|
+
)
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""CampaignOrchestrator — run all registered campaigns and aggregate results."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
|
|
8
|
+
from patchi.core.assurance.graph import AssuranceGraph
|
|
9
|
+
|
|
10
|
+
from .base import Campaign, CampaignStep
|
|
11
|
+
from .data_flow_abuse import DataFlowAbuseCampaign
|
|
12
|
+
from .state_transitions import StateTransitionCampaign
|
|
13
|
+
|
|
14
|
+
_log = logging.getLogger("patchi.core.campaigns.orchestrator")
|
|
15
|
+
|
|
16
|
+
# Registry of available campaigns
|
|
17
|
+
CAMPAIGN_REGISTRY: dict[str, type[Campaign]] = {
|
|
18
|
+
"state_transitions": StateTransitionCampaign,
|
|
19
|
+
"data_flow_abuse": DataFlowAbuseCampaign,
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class CampaignResult:
|
|
25
|
+
"""Result of running a single campaign."""
|
|
26
|
+
|
|
27
|
+
name: str
|
|
28
|
+
steps: list[CampaignStep]
|
|
29
|
+
total_findings: int = 0
|
|
30
|
+
critical: int = 0
|
|
31
|
+
high: int = 0
|
|
32
|
+
|
|
33
|
+
def to_dict(self) -> dict:
|
|
34
|
+
return {
|
|
35
|
+
"name": self.name,
|
|
36
|
+
"steps": [s.to_dict() for s in self.steps],
|
|
37
|
+
"total_findings": self.total_findings,
|
|
38
|
+
"critical": self.critical,
|
|
39
|
+
"high": self.high,
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass
|
|
44
|
+
class OrchestratorResult:
|
|
45
|
+
"""Aggregated result of all campaigns."""
|
|
46
|
+
|
|
47
|
+
campaigns: list[CampaignResult]
|
|
48
|
+
total_findings: int = 0
|
|
49
|
+
critical: int = 0
|
|
50
|
+
high: int = 0
|
|
51
|
+
|
|
52
|
+
def to_dict(self) -> dict:
|
|
53
|
+
return {
|
|
54
|
+
"campaigns": [c.to_dict() for c in self.campaigns],
|
|
55
|
+
"total_findings": self.total_findings,
|
|
56
|
+
"critical": self.critical,
|
|
57
|
+
"high": self.high,
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class CampaignOrchestrator:
|
|
62
|
+
"""Orchestrate all registered campaigns against the assurance graph."""
|
|
63
|
+
|
|
64
|
+
def __init__(self, graph: AssuranceGraph):
|
|
65
|
+
self._graph = graph
|
|
66
|
+
|
|
67
|
+
def run_all(self) -> OrchestratorResult:
|
|
68
|
+
"""Run all registered campaigns."""
|
|
69
|
+
results: list[CampaignResult] = []
|
|
70
|
+
total_findings = 0
|
|
71
|
+
critical = 0
|
|
72
|
+
high = 0
|
|
73
|
+
|
|
74
|
+
for name, cls in CAMPAIGN_REGISTRY.items():
|
|
75
|
+
try:
|
|
76
|
+
campaign = cls(self._graph)
|
|
77
|
+
steps = campaign.run()
|
|
78
|
+
|
|
79
|
+
campaign_findings = 0
|
|
80
|
+
campaign_critical = 0
|
|
81
|
+
campaign_high = 0
|
|
82
|
+
for step in steps:
|
|
83
|
+
campaign_findings += len(step.findings)
|
|
84
|
+
for f in step.findings:
|
|
85
|
+
sev = f.get("severity", "")
|
|
86
|
+
if sev == "critical":
|
|
87
|
+
campaign_critical += 1
|
|
88
|
+
elif sev == "high":
|
|
89
|
+
campaign_high += 1
|
|
90
|
+
|
|
91
|
+
result = CampaignResult(
|
|
92
|
+
name=name,
|
|
93
|
+
steps=steps,
|
|
94
|
+
total_findings=campaign_findings,
|
|
95
|
+
critical=campaign_critical,
|
|
96
|
+
high=campaign_high,
|
|
97
|
+
)
|
|
98
|
+
results.append(result)
|
|
99
|
+
total_findings += campaign_findings
|
|
100
|
+
critical += campaign_critical
|
|
101
|
+
high += campaign_high
|
|
102
|
+
except Exception as e:
|
|
103
|
+
_log.warning("Campaign %s failed: %s", name, e)
|
|
104
|
+
results.append(
|
|
105
|
+
CampaignResult(
|
|
106
|
+
name=name,
|
|
107
|
+
steps=[CampaignStep(name="error", description=str(e), status="failed")],
|
|
108
|
+
total_findings=1,
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
return OrchestratorResult(
|
|
113
|
+
campaigns=results,
|
|
114
|
+
total_findings=total_findings,
|
|
115
|
+
critical=critical,
|
|
116
|
+
high=high,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
def run_campaign(self, name: str) -> CampaignResult:
|
|
120
|
+
"""Run a specific campaign by name."""
|
|
121
|
+
cls = CAMPAIGN_REGISTRY.get(name)
|
|
122
|
+
if cls is None:
|
|
123
|
+
raise ValueError(
|
|
124
|
+
f"Unknown campaign: {name}. Available: {list(CAMPAIGN_REGISTRY.keys())}"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
campaign = cls(self._graph)
|
|
128
|
+
steps = campaign.run()
|
|
129
|
+
|
|
130
|
+
total = sum(len(s.findings) for s in steps)
|
|
131
|
+
critical = sum(1 for s in steps for f in s.findings if f.get("severity") == "critical")
|
|
132
|
+
high = sum(1 for s in steps for f in s.findings if f.get("severity") == "high")
|
|
133
|
+
|
|
134
|
+
return CampaignResult(
|
|
135
|
+
name=name,
|
|
136
|
+
steps=steps,
|
|
137
|
+
total_findings=total,
|
|
138
|
+
critical=critical,
|
|
139
|
+
high=high,
|
|
140
|
+
)
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""StateTransitionCampaign — test application state machine security.
|
|
2
|
+
|
|
3
|
+
Discovers states, maps transitions, tests legal/illegal sequences,
|
|
4
|
+
checks for replay attacks and concurrent race conditions.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
|
|
11
|
+
from .base import Campaign, CampaignStep
|
|
12
|
+
|
|
13
|
+
_log = logging.getLogger("patchi.core.campaigns.state_transitions")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class StateTransitionCampaign(Campaign):
|
|
17
|
+
"""Test state machine security: legal transitions, illegal transitions, replay, concurrency."""
|
|
18
|
+
|
|
19
|
+
name = "state_transitions"
|
|
20
|
+
description = "Tests application state machine security"
|
|
21
|
+
|
|
22
|
+
def steps(self) -> list[CampaignStep]:
|
|
23
|
+
return [
|
|
24
|
+
CampaignStep(
|
|
25
|
+
name="discover_states", description="Discover all application states from the graph"
|
|
26
|
+
),
|
|
27
|
+
CampaignStep(name="map_transitions", description="Map all legal state transitions"),
|
|
28
|
+
CampaignStep(
|
|
29
|
+
name="identify_guards", description="Identify guards protecting state transitions"
|
|
30
|
+
),
|
|
31
|
+
CampaignStep(
|
|
32
|
+
name="test_legal_sequences", description="Test legal state transition sequences"
|
|
33
|
+
),
|
|
34
|
+
CampaignStep(
|
|
35
|
+
name="test_illegal_sequences", description="Test illegal state transition sequences"
|
|
36
|
+
),
|
|
37
|
+
CampaignStep(
|
|
38
|
+
name="test_replay", description="Test replay attacks on state transitions"
|
|
39
|
+
),
|
|
40
|
+
CampaignStep(name="test_concurrent", description="Test concurrent state transitions"),
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
def _execute_step(self, step: CampaignStep) -> None:
|
|
44
|
+
if step.name == "discover_states":
|
|
45
|
+
self._discover_states(step)
|
|
46
|
+
elif step.name == "map_transitions":
|
|
47
|
+
self._map_transitions(step)
|
|
48
|
+
elif step.name == "identify_guards":
|
|
49
|
+
self._identify_guards(step)
|
|
50
|
+
elif step.name == "test_legal_sequences":
|
|
51
|
+
self._test_legal_sequences(step)
|
|
52
|
+
elif step.name == "test_illegal_sequences":
|
|
53
|
+
self._test_illegal_sequences(step)
|
|
54
|
+
elif step.name == "test_replay":
|
|
55
|
+
self._test_replay(step)
|
|
56
|
+
elif step.name == "test_concurrent":
|
|
57
|
+
self._test_concurrent(step)
|
|
58
|
+
|
|
59
|
+
def _discover_states(self, step: CampaignStep) -> None:
|
|
60
|
+
"""Find all states mentioned in the assurance graph."""
|
|
61
|
+
states: set[str] = set()
|
|
62
|
+
for claim in self._graph.claims.values():
|
|
63
|
+
for ev in claim.evidence:
|
|
64
|
+
a = ev.artifact
|
|
65
|
+
if "from_state" in a:
|
|
66
|
+
states.add(a["from_state"])
|
|
67
|
+
if "to_state" in a:
|
|
68
|
+
states.add(a["to_state"])
|
|
69
|
+
|
|
70
|
+
if not states:
|
|
71
|
+
step.findings.append(
|
|
72
|
+
{
|
|
73
|
+
"type": "no_states_found",
|
|
74
|
+
"severity": "info",
|
|
75
|
+
"detail": "No application states discovered from the assurance graph",
|
|
76
|
+
}
|
|
77
|
+
)
|
|
78
|
+
else:
|
|
79
|
+
step.findings.append(
|
|
80
|
+
{
|
|
81
|
+
"type": "states_discovered",
|
|
82
|
+
"severity": "info",
|
|
83
|
+
"detail": f"Found {len(states)} states: {', '.join(sorted(states))}",
|
|
84
|
+
}
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
def _map_transitions(self, step: CampaignStep) -> None:
|
|
88
|
+
"""Map all legal transitions and check for completeness."""
|
|
89
|
+
transitions: list[dict] = []
|
|
90
|
+
for claim in self._graph.claims.values():
|
|
91
|
+
for ev in claim.evidence:
|
|
92
|
+
a = ev.artifact
|
|
93
|
+
if "from_state" in a and "to_state" in a:
|
|
94
|
+
transitions.append(
|
|
95
|
+
{
|
|
96
|
+
"from": a["from_state"],
|
|
97
|
+
"to": a["to_state"],
|
|
98
|
+
"action": a.get("action", "unknown"),
|
|
99
|
+
"guarded": a.get("guarded", False),
|
|
100
|
+
}
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Check for transitions without guards
|
|
104
|
+
unguarded = [t for t in transitions if not t["guarded"]]
|
|
105
|
+
if unguarded:
|
|
106
|
+
for t in unguarded:
|
|
107
|
+
step.findings.append(
|
|
108
|
+
{
|
|
109
|
+
"type": "unguarded_transition",
|
|
110
|
+
"severity": "medium",
|
|
111
|
+
"detail": f"Unguarded transition: {t['from']} → {t['to']} via {t['action']}",
|
|
112
|
+
}
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def _identify_guards(self, step: CampaignStep) -> None:
|
|
116
|
+
"""Check that transitions have authorization guards."""
|
|
117
|
+
for claim in self._graph.claims.values():
|
|
118
|
+
if "guard" in claim.statement.lower() or "auth" in claim.statement.lower():
|
|
119
|
+
if claim.verdict.value == "disproved":
|
|
120
|
+
step.findings.append(
|
|
121
|
+
{
|
|
122
|
+
"type": "guard_broken",
|
|
123
|
+
"severity": "critical",
|
|
124
|
+
"detail": f"Guard violated: {claim.statement}",
|
|
125
|
+
}
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
def _test_legal_sequences(self, step: CampaignStep) -> None:
|
|
129
|
+
"""Test that legal transition sequences don't bypass guards."""
|
|
130
|
+
# Check for claims about legal sequences
|
|
131
|
+
for claim in self._graph.claims.values():
|
|
132
|
+
if "sequence" in claim.statement.lower() and claim.verdict.value == "disproved":
|
|
133
|
+
step.findings.append(
|
|
134
|
+
{
|
|
135
|
+
"type": "sequence_bypass",
|
|
136
|
+
"severity": "high",
|
|
137
|
+
"detail": f"Legal sequence bypasses guard: {claim.statement}",
|
|
138
|
+
}
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
def _test_illegal_sequences(self, step: CampaignStep) -> None:
|
|
142
|
+
"""Test that illegal transitions are properly blocked."""
|
|
143
|
+
# Find claims about forbidden transitions
|
|
144
|
+
for claim in self._graph.claims.values():
|
|
145
|
+
if "must not" in claim.statement.lower() or "forbidden" in claim.statement.lower():
|
|
146
|
+
if claim.verdict.value == "disproved":
|
|
147
|
+
step.findings.append(
|
|
148
|
+
{
|
|
149
|
+
"type": "illegal_transition_allowed",
|
|
150
|
+
"severity": "critical",
|
|
151
|
+
"detail": f"Forbidden transition is possible: {claim.statement}",
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
def _test_replay(self, step: CampaignStep) -> None:
|
|
156
|
+
"""Test for replay vulnerabilities in state transitions."""
|
|
157
|
+
for claim in self._graph.claims.values():
|
|
158
|
+
if "replay" in claim.statement.lower() and claim.verdict.value == "disproved":
|
|
159
|
+
step.findings.append(
|
|
160
|
+
{
|
|
161
|
+
"type": "replay_vulnerability",
|
|
162
|
+
"severity": "high",
|
|
163
|
+
"detail": f"Replay attack possible: {claim.statement}",
|
|
164
|
+
}
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
def _test_concurrent(self, step: CampaignStep) -> None:
|
|
168
|
+
"""Test for concurrent state transition race conditions."""
|
|
169
|
+
for claim in self._graph.claims.values():
|
|
170
|
+
if "concurrent" in claim.statement.lower() or "race" in claim.statement.lower():
|
|
171
|
+
if claim.verdict.value == "disproved":
|
|
172
|
+
step.findings.append(
|
|
173
|
+
{
|
|
174
|
+
"type": "race_condition",
|
|
175
|
+
"severity": "high",
|
|
176
|
+
"detail": f"Race condition found: {claim.statement}",
|
|
177
|
+
}
|
|
178
|
+
)
|
patchi/core/ci_bundle.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"""
|
|
2
|
+
CI/PR Bundle — Finding stable id + Baseline+delta + --since + SARIF + fix --safe-all.
|
|
3
|
+
|
|
4
|
+
Stable id: hash(file+type+line//5) for dedup across runs.
|
|
5
|
+
Renderer registry: markdown/json/sarif renderers via register_renderer().
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
import json
|
|
12
|
+
from collections.abc import Callable
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
def stable_id(finding: dict) -> str:
|
|
16
|
+
raw = f"{finding.get('file','')}:{finding.get('type','')}:{finding.get('line',0)//5}"
|
|
17
|
+
return hashlib.sha256(raw.encode()).hexdigest()[:12]
|
|
18
|
+
|
|
19
|
+
def baseline_delta(baseline: list[dict], current: list[dict]) -> dict:
|
|
20
|
+
b_ids = {stable_id(f) for f in baseline}
|
|
21
|
+
c_ids = {stable_id(f) for f in current}
|
|
22
|
+
added = [f for f in current if stable_id(f) not in b_ids]
|
|
23
|
+
fixed = [f for f in baseline if stable_id(f) not in c_ids]
|
|
24
|
+
return {"added": added, "fixed": fixed, "added_count": len(added), "fixed_count": len(fixed)}
|
|
25
|
+
|
|
26
|
+
def filter_since(findings: list[dict], since_ref: str, root: Path) -> list[dict]:
|
|
27
|
+
"""Filter findings to files changed since git ref."""
|
|
28
|
+
try:
|
|
29
|
+
import subprocess
|
|
30
|
+
out = subprocess.run(["git","diff","--name-only", since_ref], capture_output=True, text=True, timeout=5, cwd=str(root))
|
|
31
|
+
if out.returncode != 0:
|
|
32
|
+
return findings
|
|
33
|
+
changed = set(out.stdout.splitlines())
|
|
34
|
+
return [f for f in findings if f.get("file","") in changed]
|
|
35
|
+
except Exception:
|
|
36
|
+
return findings
|
|
37
|
+
|
|
38
|
+
def to_sarif(findings: list[dict], root: Path | None = None) -> dict:
|
|
39
|
+
"""Single SARIF implementation — delegates to export.sarif.
|
|
40
|
+
|
|
41
|
+
(root kept for compat; SARIF carries no repo-root field.)
|
|
42
|
+
"""
|
|
43
|
+
from patchi.core.export.sarif import convert_findings
|
|
44
|
+
|
|
45
|
+
return convert_findings(findings)
|
|
46
|
+
|
|
47
|
+
def write_sarif(findings: list[dict], out_path: Path) -> None:
|
|
48
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
49
|
+
out_path.write_text(json.dumps(to_sarif(findings), indent=2), encoding="utf-8")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def render_markdown(findings: list[dict]) -> str:
|
|
53
|
+
lines = ["# Patchi Findings", ""]
|
|
54
|
+
order = {"critical": 0, "high": 1, "medium": 2, "low": 3, "info": 4}
|
|
55
|
+
def _sort_key(d: dict) -> tuple:
|
|
56
|
+
return (order.get(d.get("severity", "info"), 5), d.get("file", ""))
|
|
57
|
+
for f in sorted(findings, key=_sort_key):
|
|
58
|
+
lines.append(
|
|
59
|
+
f"- `{f.get('file', '?')}:{f.get('line', 0)}` "
|
|
60
|
+
f"[{f.get('severity', 'info')}] {f.get('type', '?')} — {f.get('message', '')}"
|
|
61
|
+
)
|
|
62
|
+
return "\n".join(lines) + "\n"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
RENDERERS: dict[str, Callable[[list[dict]], str]] = {
|
|
66
|
+
"markdown": render_markdown,
|
|
67
|
+
"json": lambda findings: json.dumps(findings, indent=2),
|
|
68
|
+
"sarif": lambda findings: json.dumps(to_sarif(findings), indent=2),
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
_SEV_ORDER = {"critical": 0, "high": 1, "medium": 2, "low": 3, "info": 4}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def exit_code_for(findings: list[dict], fail_on: str | None) -> int:
|
|
76
|
+
"""CI gate: 1 when any finding meets the --fail-on severity, else 0.
|
|
77
|
+
|
|
78
|
+
No threshold (None) never fails — plain `p scan` stays exit 0.
|
|
79
|
+
"""
|
|
80
|
+
if not fail_on:
|
|
81
|
+
return 0
|
|
82
|
+
try:
|
|
83
|
+
bar = _SEV_ORDER[fail_on.lower()]
|
|
84
|
+
except KeyError:
|
|
85
|
+
raise ValueError(f"unknown severity {fail_on!r} (have: {sorted(_SEV_ORDER)})") from None
|
|
86
|
+
for f in findings:
|
|
87
|
+
sev = f.get("severity", "info")
|
|
88
|
+
sev = sev.value if hasattr(sev, "value") else str(sev)
|
|
89
|
+
if _SEV_ORDER.get(sev.lower(), 5) <= bar:
|
|
90
|
+
return 1
|
|
91
|
+
return 0
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def register_renderer(name: str, fn: Callable[[list[dict]], str]) -> None:
|
|
95
|
+
"""Add or override a findings renderer (e.g. plugins)."""
|
|
96
|
+
RENDERERS[name.lower()] = fn
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def render_findings(findings: list[dict], fmt: str = "markdown") -> str:
|
|
100
|
+
try:
|
|
101
|
+
renderer = RENDERERS[fmt.lower()]
|
|
102
|
+
except KeyError:
|
|
103
|
+
raise ValueError(f"unknown findings format {fmt!r} (have: {sorted(RENDERERS)})") from None
|
|
104
|
+
return renderer(findings)
|