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,106 @@
|
|
|
1
|
+
"""
|
|
2
|
+
RunAgent — starts the target app after P-Check green, for Test/Attack.
|
|
3
|
+
|
|
4
|
+
Chain: Check (Install/Build/Format) → green READY_TO_SERVE → Run (AppLauncher) → Test/Attack
|
|
5
|
+
Test/Attack agents rely on Run, Run relies on Check.
|
|
6
|
+
|
|
7
|
+
This agent is always run anytime a Test or Attack agent is queued.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import logging
|
|
13
|
+
|
|
14
|
+
from patchi.core.agents.base import (
|
|
15
|
+
AgentGroup,
|
|
16
|
+
AgentInput,
|
|
17
|
+
AgentResult,
|
|
18
|
+
AgentStatus,
|
|
19
|
+
BaseAgent,
|
|
20
|
+
Severity,
|
|
21
|
+
make_finding,
|
|
22
|
+
register,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
_log = logging.getLogger("patchi.agents.run")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@register
|
|
29
|
+
class RunAgent(BaseAgent):
|
|
30
|
+
group = AgentGroup.SCANNER
|
|
31
|
+
name = "RunAgent"
|
|
32
|
+
description = "Run — start app on free port after P-Check green (test/attack dependency)"
|
|
33
|
+
timeout = 90
|
|
34
|
+
|
|
35
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
36
|
+
# Gate: Check must be green
|
|
37
|
+
try:
|
|
38
|
+
from patchi.core.testing.gate import require_ready
|
|
39
|
+
|
|
40
|
+
ready, url, st = require_ready(inp.root)
|
|
41
|
+
if not ready:
|
|
42
|
+
from patchi.core.testing.gate import gate_message
|
|
43
|
+
|
|
44
|
+
msg = gate_message(st)
|
|
45
|
+
result.status = AgentStatus.SKIPPED
|
|
46
|
+
result.data["gate_blocked"] = True
|
|
47
|
+
result.data["gate_reason"] = msg
|
|
48
|
+
result.add_finding(
|
|
49
|
+
make_finding(
|
|
50
|
+
severity=Severity.INFO,
|
|
51
|
+
file="",
|
|
52
|
+
line_start=0,
|
|
53
|
+
title="Run skipped — P-Check not READY_TO_SERVE",
|
|
54
|
+
description=msg,
|
|
55
|
+
finding_type="run_gate_blocked",
|
|
56
|
+
)
|
|
57
|
+
)
|
|
58
|
+
return
|
|
59
|
+
except Exception as exc: # noqa: BLE001
|
|
60
|
+
_log.debug("run gate check failed: %s", exc)
|
|
61
|
+
|
|
62
|
+
# Start app
|
|
63
|
+
try:
|
|
64
|
+
from patchi.core.testing.app_launcher import ensure_running
|
|
65
|
+
|
|
66
|
+
# Reuse if already running
|
|
67
|
+
url = ensure_running(inp.root, inp.config, inp.extra)
|
|
68
|
+
if url:
|
|
69
|
+
result.status = AgentStatus.SUCCEEDED
|
|
70
|
+
result.data["base_url"] = url
|
|
71
|
+
result.data["url"] = url
|
|
72
|
+
result.add_finding(
|
|
73
|
+
make_finding(
|
|
74
|
+
severity=Severity.INFO,
|
|
75
|
+
file="",
|
|
76
|
+
line_start=0,
|
|
77
|
+
title=f"App running at {url}",
|
|
78
|
+
description=f"RunAgent started target app at {url} (P-Check was green)",
|
|
79
|
+
finding_type="app_running",
|
|
80
|
+
)
|
|
81
|
+
)
|
|
82
|
+
else:
|
|
83
|
+
result.status = AgentStatus.FAILED
|
|
84
|
+
result.add_finding(
|
|
85
|
+
make_finding(
|
|
86
|
+
severity=Severity.HIGH,
|
|
87
|
+
file="",
|
|
88
|
+
line_start=0,
|
|
89
|
+
title="Run failed — could not start app",
|
|
90
|
+
description="AppLauncher ensure_running returned None — check .patchi/launcher/app.log and start command",
|
|
91
|
+
finding_type="run_failed",
|
|
92
|
+
)
|
|
93
|
+
)
|
|
94
|
+
except Exception as exc: # noqa: BLE001
|
|
95
|
+
_log.warning("RunAgent failed: %s", exc)
|
|
96
|
+
result.status = AgentStatus.FAILED
|
|
97
|
+
result.add_finding(
|
|
98
|
+
make_finding(
|
|
99
|
+
severity=Severity.HIGH,
|
|
100
|
+
file="",
|
|
101
|
+
line_start=0,
|
|
102
|
+
title="Run exception",
|
|
103
|
+
description=str(exc)[:300],
|
|
104
|
+
finding_type="run_exception",
|
|
105
|
+
)
|
|
106
|
+
)
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
"""
|
|
2
|
+
RustUnwrapAgent §8.2.4 — clippy style unwrap/expect audit.
|
|
3
|
+
|
|
4
|
+
Finds .unwrap() / .expect() without descriptive message, suggests ?, expect("msg") or pattern match.
|
|
5
|
+
Optionally runs `cargo clippy -- -W clippy::unwrap_used` if cargo available.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import re
|
|
12
|
+
import shutil
|
|
13
|
+
import subprocess
|
|
14
|
+
|
|
15
|
+
from patchi.core.agents.base import (
|
|
16
|
+
AgentGroup,
|
|
17
|
+
AgentInput,
|
|
18
|
+
AgentResult,
|
|
19
|
+
AgentStatus,
|
|
20
|
+
BaseAgent,
|
|
21
|
+
Severity,
|
|
22
|
+
make_finding,
|
|
23
|
+
register,
|
|
24
|
+
safe_rglob,
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
_log = logging.getLogger("patchi.agents.rust_unwrap")
|
|
28
|
+
|
|
29
|
+
_UNWRAP_RE = re.compile(r"\.unwrap\(\)")
|
|
30
|
+
_EXPECT_RE = re.compile(r"\.expect\(\s*\"\"|\.expect\(\s*''|\\.expect\(\s*\)")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@register
|
|
34
|
+
class RustUnwrapAgent(BaseAgent):
|
|
35
|
+
group = AgentGroup.SCANNER
|
|
36
|
+
name = "RustUnwrapAgent"
|
|
37
|
+
description = "Rust unwrap()/expect() audit §8.2.4 — clippy unwrap_used"
|
|
38
|
+
timeout = 60
|
|
39
|
+
|
|
40
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
41
|
+
# Try clippy
|
|
42
|
+
if shutil.which("cargo"):
|
|
43
|
+
try:
|
|
44
|
+
proc = subprocess.run(
|
|
45
|
+
["cargo", "clippy", "--", "-W", "clippy::unwrap_used", "-W", "clippy::expect_used"],
|
|
46
|
+
capture_output=True,
|
|
47
|
+
text=True,
|
|
48
|
+
timeout=30,
|
|
49
|
+
cwd=str(inp.root),
|
|
50
|
+
)
|
|
51
|
+
for line in (proc.stderr + proc.stdout).splitlines():
|
|
52
|
+
if "unwrap_used" in line or "expect_used" in line:
|
|
53
|
+
# clippy: file:line:col: warning: used `unwrap()`...
|
|
54
|
+
m = re.match(r"([^:]+):(\d+):\d+:\s*warning:\s*(.*)", line)
|
|
55
|
+
if m:
|
|
56
|
+
result.add_finding(
|
|
57
|
+
make_finding(
|
|
58
|
+
severity=Severity.LOW,
|
|
59
|
+
file=m.group(1),
|
|
60
|
+
line_start=int(m.group(2)),
|
|
61
|
+
title="clippy::unwrap_used",
|
|
62
|
+
description=m.group(3).strip()[:200],
|
|
63
|
+
finding_type="rust_unwrap",
|
|
64
|
+
)
|
|
65
|
+
)
|
|
66
|
+
except Exception as exc: # noqa: BLE001
|
|
67
|
+
_log.debug("clippy failed: %s", exc)
|
|
68
|
+
|
|
69
|
+
for fp in safe_rglob(inp.root, "*.rs"):
|
|
70
|
+
rel = fp.relative_to(inp.root).as_posix()
|
|
71
|
+
try:
|
|
72
|
+
txt = fp.read_text(encoding="utf-8", errors="replace")
|
|
73
|
+
except OSError:
|
|
74
|
+
continue
|
|
75
|
+
for i, line in enumerate(txt.splitlines(), 1):
|
|
76
|
+
if _UNWRAP_RE.search(line):
|
|
77
|
+
result.add_finding(
|
|
78
|
+
make_finding(
|
|
79
|
+
severity=Severity.LOW,
|
|
80
|
+
file=rel,
|
|
81
|
+
line_start=i,
|
|
82
|
+
title="Unsafe .unwrap() — panics on Err/None",
|
|
83
|
+
description="Prefer `?`, `unwrap_or`, or `.expect(\"context\")` with message. Clippy: unwrap_used.",
|
|
84
|
+
evidence=line.strip()[:120],
|
|
85
|
+
finding_type="rust_unwrap",
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
elif _EXPECT_RE.search(line):
|
|
89
|
+
result.add_finding(
|
|
90
|
+
make_finding(
|
|
91
|
+
severity=Severity.LOW,
|
|
92
|
+
file=rel,
|
|
93
|
+
line_start=i,
|
|
94
|
+
title="Empty .expect(\"\") — no context",
|
|
95
|
+
description="`.expect(\"\")` gives no diagnostic; use descriptive message.",
|
|
96
|
+
evidence=line.strip()[:120],
|
|
97
|
+
finding_type="rust_expect_empty",
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
result.status = AgentStatus.SUCCEEDED
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
"""SBOMGeneratorAgent — generates CycloneDX/SPDX Software Bill of Materials.
|
|
2
|
+
|
|
3
|
+
Covers §11.5:
|
|
4
|
+
- Parse dependency manifests (package.json, requirements.txt, Cargo.toml, go.mod, Gemfile, etc.)
|
|
5
|
+
- Generate CycloneDX 1.5-compatible JSON output
|
|
6
|
+
- Write to .patchi/sbom.cdx.json
|
|
7
|
+
|
|
8
|
+
Language-agnostic: supports npm, pip, cargo, go, bundler, composer ecosystems.
|
|
9
|
+
"""
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import json
|
|
13
|
+
import logging
|
|
14
|
+
import re
|
|
15
|
+
from datetime import UTC, datetime
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from uuid import uuid4
|
|
18
|
+
|
|
19
|
+
from .base import (
|
|
20
|
+
AgentGroup,
|
|
21
|
+
AgentInput,
|
|
22
|
+
AgentResult,
|
|
23
|
+
AgentStatus,
|
|
24
|
+
BaseAgent,
|
|
25
|
+
Severity,
|
|
26
|
+
make_finding,
|
|
27
|
+
register,
|
|
28
|
+
safe_rglob,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
_ECOSYSTEM_MAP = {
|
|
32
|
+
"package.json": ("npm", "JavaScript"),
|
|
33
|
+
"requirements.txt": ("pypi", "Python"),
|
|
34
|
+
"pyproject.toml": ("pypi", "Python"),
|
|
35
|
+
"Pipfile": ("pypi", "Python"),
|
|
36
|
+
"poetry.lock": ("pypi", "Python"),
|
|
37
|
+
"Cargo.toml": ("crates", "Rust"),
|
|
38
|
+
"Cargo.lock": ("crates", "Rust"),
|
|
39
|
+
"go.mod": ("go", "Go"),
|
|
40
|
+
"go.sum": ("go", "Go"),
|
|
41
|
+
"Gemfile": ("gem", "Ruby"),
|
|
42
|
+
"Gemfile.lock": ("gem", "Ruby"),
|
|
43
|
+
"composer.json": ("composer", "PHP"),
|
|
44
|
+
"yarn.lock": ("npm", "JavaScript"),
|
|
45
|
+
"pnpm-lock.yaml": ("npm", "JavaScript"),
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
_COMPONENT_TYPES = {
|
|
49
|
+
"npm": "library",
|
|
50
|
+
"pypi": "library",
|
|
51
|
+
"crates": "library",
|
|
52
|
+
"go": "library",
|
|
53
|
+
"gem": "library",
|
|
54
|
+
"composer": "library",
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
_log = logging.getLogger("patchi.agents.sbom_generator")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _parse_deps(fp: Path) -> list[dict]:
|
|
62
|
+
deps: list[dict] = []
|
|
63
|
+
try:
|
|
64
|
+
content = fp.read_text(encoding="utf-8")
|
|
65
|
+
except Exception as e:
|
|
66
|
+
_log.warning("_parse_deps failed: %s", e)
|
|
67
|
+
return deps
|
|
68
|
+
|
|
69
|
+
if fp.name == "package.json":
|
|
70
|
+
try:
|
|
71
|
+
data = json.loads(content)
|
|
72
|
+
all_deps = {**data.get("dependencies", {}), **data.get("devDependencies", {})}
|
|
73
|
+
for name, ver in all_deps.items():
|
|
74
|
+
deps.append({"name": name, "version": ver.lstrip("^~>=<")})
|
|
75
|
+
except json.JSONDecodeError:
|
|
76
|
+
pass
|
|
77
|
+
elif fp.name == "requirements.txt":
|
|
78
|
+
for line in content.splitlines():
|
|
79
|
+
line = line.strip()
|
|
80
|
+
if not line or line.startswith("#") or line.startswith("-"):
|
|
81
|
+
continue
|
|
82
|
+
for sep in ["==", ">=", "<="]:
|
|
83
|
+
if sep in line:
|
|
84
|
+
name, ver = line.split(sep, 1)
|
|
85
|
+
deps.append({"name": name.strip(), "version": ver.strip()})
|
|
86
|
+
break
|
|
87
|
+
else:
|
|
88
|
+
deps.append({"name": line, "version": "unknown"})
|
|
89
|
+
elif fp.name == "Cargo.toml":
|
|
90
|
+
in_deps = False
|
|
91
|
+
for line in content.splitlines():
|
|
92
|
+
stripped = line.strip()
|
|
93
|
+
if stripped.startswith("[dependencies]") or stripped.startswith("[dev-dependencies]"):
|
|
94
|
+
in_deps = True
|
|
95
|
+
continue
|
|
96
|
+
if stripped.startswith("[") and in_deps:
|
|
97
|
+
in_deps = False
|
|
98
|
+
if in_deps and "=" in stripped:
|
|
99
|
+
m = re.match(r"(\w[\w\-_]*)", stripped)
|
|
100
|
+
if m:
|
|
101
|
+
name = m.group(1)
|
|
102
|
+
ver_part = stripped[len(name) :].strip("= ").strip('"')
|
|
103
|
+
deps.append({"name": name, "version": ver_part})
|
|
104
|
+
elif fp.name == "pyproject.toml":
|
|
105
|
+
in_deps = False
|
|
106
|
+
for line in content.splitlines():
|
|
107
|
+
stripped = line.strip()
|
|
108
|
+
if stripped.startswith("dependencies = ["):
|
|
109
|
+
in_deps = True
|
|
110
|
+
continue
|
|
111
|
+
if in_deps:
|
|
112
|
+
if stripped == "]":
|
|
113
|
+
in_deps = False
|
|
114
|
+
else:
|
|
115
|
+
m = re.match(r'["\']([^"\'>]+?)([>=<~!]+)([^"\']+)["\'],?', stripped)
|
|
116
|
+
if m:
|
|
117
|
+
deps.append({"name": m.group(1), "version": m.group(3)})
|
|
118
|
+
elif fp.name == "go.mod":
|
|
119
|
+
for line in content.splitlines():
|
|
120
|
+
stripped = line.strip()
|
|
121
|
+
if stripped.startswith("require ") or stripped.startswith("\trequire "):
|
|
122
|
+
parts = stripped.replace("require ", "").split()
|
|
123
|
+
if len(parts) >= 2:
|
|
124
|
+
deps.append({"name": parts[0], "version": parts[1]})
|
|
125
|
+
return deps
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _build_cyclonedx(
|
|
129
|
+
root: Path,
|
|
130
|
+
deps_by_eco: dict[str, list[dict]],
|
|
131
|
+
) -> dict:
|
|
132
|
+
components: list[dict] = []
|
|
133
|
+
for eco, dep_list in deps_by_eco.items():
|
|
134
|
+
comp_type = _COMPONENT_TYPES.get(eco, "library")
|
|
135
|
+
for dep in dep_list:
|
|
136
|
+
components.append(
|
|
137
|
+
{
|
|
138
|
+
"type": comp_type,
|
|
139
|
+
"name": dep["name"],
|
|
140
|
+
"version": dep["version"],
|
|
141
|
+
"purl": f"pkg:{eco}/{dep['name']}@{dep['version']}",
|
|
142
|
+
"bom-ref": f"pkg:{eco}/{dep['name']}@{dep['version']}",
|
|
143
|
+
}
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
"bomFormat": "CycloneDX",
|
|
148
|
+
"specVersion": "1.5",
|
|
149
|
+
"serialNumber": f"urn:uuid:{uuid4().hex}",
|
|
150
|
+
"version": 1,
|
|
151
|
+
"metadata": {
|
|
152
|
+
"timestamp": datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
153
|
+
"tools": [{"name": "Patchi", "version": "1.0"}],
|
|
154
|
+
"component": {
|
|
155
|
+
"type": "application",
|
|
156
|
+
"name": root.name,
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
"components": components,
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@register
|
|
164
|
+
class SBOMGeneratorAgent(BaseAgent):
|
|
165
|
+
"""Generates CycloneDX Software Bill of Materials from dependency manifests."""
|
|
166
|
+
|
|
167
|
+
group = AgentGroup.SCANNER
|
|
168
|
+
name = "SBOMGeneratorAgent"
|
|
169
|
+
description = "Generate CycloneDX SBOM from package.json, requirements.txt, Cargo.toml, go.mod, Gemfile, etc."
|
|
170
|
+
|
|
171
|
+
def _try_external(self, inp: AgentInput) -> dict | None:
|
|
172
|
+
"""Try cdxgen then syft as subprocess — 20+ ecos depth, fail-open to internal parser."""
|
|
173
|
+
import shutil
|
|
174
|
+
import subprocess as _sp
|
|
175
|
+
|
|
176
|
+
# cdxgen: npx @cyclonedx/cdxgen -o /tmp/sbom.json
|
|
177
|
+
for cmd in (
|
|
178
|
+
["npx", "--yes", "@cyclonedx/cdxgen", "-o", str(inp.root / ".patchi" / "sbom.cdxgen.json"), "--no-recurse"],
|
|
179
|
+
["syft", str(inp.root), "-o", "cyclonedx-json"],
|
|
180
|
+
):
|
|
181
|
+
if not shutil.which(cmd[0]):
|
|
182
|
+
continue
|
|
183
|
+
try:
|
|
184
|
+
proc = _sp.run(cmd, capture_output=True, text=True, timeout=120, cwd=str(inp.root))
|
|
185
|
+
out = proc.stdout.strip()
|
|
186
|
+
# cdxgen writes file, syft prints json
|
|
187
|
+
cand = inp.root / ".patchi" / "sbom.cdxgen.json"
|
|
188
|
+
if cand.exists():
|
|
189
|
+
return json.loads(cand.read_text(encoding="utf-8"))
|
|
190
|
+
if out and out.startswith("{"):
|
|
191
|
+
return json.loads(out)
|
|
192
|
+
except Exception as e:
|
|
193
|
+
_log.debug("SBOM external %s failed: %s", cmd[0], e)
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
197
|
+
# Try external depth first
|
|
198
|
+
ext = self._try_external(inp)
|
|
199
|
+
if ext and ext.get("components"):
|
|
200
|
+
sbom = ext
|
|
201
|
+
total = len(ext.get("components", []))
|
|
202
|
+
sbom_dir = inp.root / ".patchi"
|
|
203
|
+
sbom_dir.mkdir(parents=True, exist_ok=True)
|
|
204
|
+
sbom_path = sbom_dir / "sbom.cdx.json"
|
|
205
|
+
try:
|
|
206
|
+
sbom_path.write_text(json.dumps(sbom, indent=2), encoding="utf-8")
|
|
207
|
+
result.findings.append(
|
|
208
|
+
make_finding(
|
|
209
|
+
self.name, "sbom_generated", Severity.INFO, str(sbom_path.relative_to(inp.root)), f"SBOM (external) {total} components"
|
|
210
|
+
)
|
|
211
|
+
)
|
|
212
|
+
except Exception as e:
|
|
213
|
+
result.add_error(f"Failed to write SBOM: {e}")
|
|
214
|
+
result.data["sbom_path"] = str(sbom_path)
|
|
215
|
+
result.data["total_components"] = total
|
|
216
|
+
result.data["ecosystems"] = ["external"]
|
|
217
|
+
result.data["sbom"] = sbom
|
|
218
|
+
result.data["external"] = True
|
|
219
|
+
result.status = AgentStatus.DONE
|
|
220
|
+
return
|
|
221
|
+
|
|
222
|
+
deps_by_eco: dict[str, list[dict]] = {}
|
|
223
|
+
files_scanned = 0
|
|
224
|
+
|
|
225
|
+
for manifest in _ECOSYSTEM_MAP:
|
|
226
|
+
for fp in safe_rglob(inp.root, manifest):
|
|
227
|
+
rel = fp.relative_to(inp.root).as_posix()
|
|
228
|
+
eco, _ = _ECOSYSTEM_MAP[manifest]
|
|
229
|
+
deps = _parse_deps(fp)
|
|
230
|
+
if deps:
|
|
231
|
+
deps_by_eco.setdefault(eco, []).extend(deps)
|
|
232
|
+
result.findings.append(
|
|
233
|
+
make_finding(
|
|
234
|
+
self.name,
|
|
235
|
+
"sbom_source",
|
|
236
|
+
Severity.INFO,
|
|
237
|
+
rel,
|
|
238
|
+
f"SBOM source: {rel} ({len(deps)} deps)",
|
|
239
|
+
)
|
|
240
|
+
)
|
|
241
|
+
files_scanned += 1
|
|
242
|
+
|
|
243
|
+
if not deps_by_eco:
|
|
244
|
+
result.findings.append(
|
|
245
|
+
make_finding(
|
|
246
|
+
self.name,
|
|
247
|
+
"no_sbom_sources",
|
|
248
|
+
Severity.INFO,
|
|
249
|
+
"",
|
|
250
|
+
"No dependency manifests found — cannot generate SBOM",
|
|
251
|
+
)
|
|
252
|
+
)
|
|
253
|
+
result.status = AgentStatus.DONE
|
|
254
|
+
return
|
|
255
|
+
|
|
256
|
+
sbom = _build_cyclonedx(inp.root, deps_by_eco)
|
|
257
|
+
total_deps = sum(len(d) for d in deps_by_eco.values())
|
|
258
|
+
|
|
259
|
+
# Write SBOM to .patchi/ directory
|
|
260
|
+
sbom_dir = inp.root / ".patchi"
|
|
261
|
+
sbom_dir.mkdir(parents=True, exist_ok=True)
|
|
262
|
+
sbom_path = sbom_dir / "sbom.cdx.json"
|
|
263
|
+
try:
|
|
264
|
+
sbom_path.write_text(json.dumps(sbom, indent=2), encoding="utf-8")
|
|
265
|
+
result.findings.append(
|
|
266
|
+
make_finding(
|
|
267
|
+
self.name,
|
|
268
|
+
"sbom_generated",
|
|
269
|
+
Severity.INFO,
|
|
270
|
+
str(sbom_path.relative_to(inp.root)),
|
|
271
|
+
f"SBOM generated: {total_deps} components across {len(deps_by_eco)} ecosystems",
|
|
272
|
+
detail=str(sbom_path),
|
|
273
|
+
)
|
|
274
|
+
)
|
|
275
|
+
except Exception as e:
|
|
276
|
+
result.add_error(f"Failed to write SBOM: {e}")
|
|
277
|
+
|
|
278
|
+
result.data["sbom_path"] = str(sbom_path)
|
|
279
|
+
result.data["total_components"] = total_deps
|
|
280
|
+
result.data["ecosystems"] = list(deps_by_eco.keys())
|
|
281
|
+
result.data["sbom"] = sbom
|
|
282
|
+
result.files_scanned = files_scanned
|
|
283
|
+
if result.status == AgentStatus.RUNNING:
|
|
284
|
+
result.status = AgentStatus.DONE
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""
|
|
2
|
+
All 20 scanner agents for Patchi.
|
|
3
|
+
|
|
4
|
+
This file has been split into separate modules for better maintainability:
|
|
5
|
+
- core_scanner.py
|
|
6
|
+
- side_file_scanner.py
|
|
7
|
+
- ui_scanner.py
|
|
8
|
+
- dependency_scanner.py
|
|
9
|
+
- test_scanner.py
|
|
10
|
+
- dead_code_scanner.py
|
|
11
|
+
- env_scanner.py
|
|
12
|
+
- route_graph_scanner.py
|
|
13
|
+
- type_scanner.py
|
|
14
|
+
- comment_scanner.py
|
|
15
|
+
- duplicate_scanner.py
|
|
16
|
+
|
|
17
|
+
Each agent inherits BaseAgent, implements _run(), and is registered
|
|
18
|
+
via the @register decorator so the coordinator can find it by name.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
from .base import register
|
|
24
|
+
from .build_tool_validator import BuildToolValidatorAgent
|
|
25
|
+
from .cicd_generator import CICDGeneratorAgent
|
|
26
|
+
from .comment_scanner import CommentScanner
|
|
27
|
+
from .core_scanner import CoreScanner
|
|
28
|
+
from .coverage_prioritizer import CoveragePrioritizerAgent
|
|
29
|
+
from .dead_code_hygiene import DeadCodeHygieneAgent
|
|
30
|
+
from .dead_code_scanner import DeadCodeScanner
|
|
31
|
+
from .dependency_scanner import DependencyScanner
|
|
32
|
+
from .duplicate_scanner import DuplicateScanner
|
|
33
|
+
from .env_scanner import EnvScanner
|
|
34
|
+
from .license_compliance import LicenseComplianceAgent
|
|
35
|
+
from .refactoring_agent import RefactoringAgent
|
|
36
|
+
from .route_graph_scanner import RouteGraphScanner
|
|
37
|
+
from .sbom_generator import SBOMGeneratorAgent
|
|
38
|
+
from .side_file_scanner import SideFileScanner
|
|
39
|
+
from .snapshot_drift_detector import SnapshotDriftDetectorAgent
|
|
40
|
+
from .spa_route_inventory import SPARouteInventoryAgent
|
|
41
|
+
from .test_scanner import TestScanner
|
|
42
|
+
from .type_scanner import TypeScanner
|
|
43
|
+
from .ui_scanner import UIScanner
|
|
44
|
+
|
|
45
|
+
__all__ = [
|
|
46
|
+
"CoreScanner",
|
|
47
|
+
"SideFileScanner",
|
|
48
|
+
"UIScanner",
|
|
49
|
+
"DependencyScanner",
|
|
50
|
+
"TestScanner",
|
|
51
|
+
"DeadCodeScanner",
|
|
52
|
+
"DeadCodeHygieneAgent",
|
|
53
|
+
"RefactoringAgent",
|
|
54
|
+
"SBOMGeneratorAgent",
|
|
55
|
+
"CICDGeneratorAgent",
|
|
56
|
+
"SPARouteInventoryAgent",
|
|
57
|
+
"CoveragePrioritizerAgent",
|
|
58
|
+
"BuildToolValidatorAgent",
|
|
59
|
+
"LicenseComplianceAgent",
|
|
60
|
+
"SnapshotDriftDetectorAgent",
|
|
61
|
+
"EnvScanner",
|
|
62
|
+
"RouteGraphScanner",
|
|
63
|
+
"TypeScanner",
|
|
64
|
+
"CommentScanner",
|
|
65
|
+
"DuplicateScanner",
|
|
66
|
+
"register",
|
|
67
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Side agents — preflight install/build/format before app run."""
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""BuildAgent §p check — npm run build / tsc --noEmit / cargo check."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
|
|
9
|
+
from patchi.core.agents.base import (
|
|
10
|
+
AgentGroup,
|
|
11
|
+
AgentInput,
|
|
12
|
+
AgentResult,
|
|
13
|
+
AgentStatus,
|
|
14
|
+
BaseAgent,
|
|
15
|
+
Severity,
|
|
16
|
+
make_finding,
|
|
17
|
+
register,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
_log = logging.getLogger("patchi.agents.side.build")
|
|
21
|
+
|
|
22
|
+
@register
|
|
23
|
+
class BuildAgent(BaseAgent):
|
|
24
|
+
group = AgentGroup.SCANNER
|
|
25
|
+
name = "BuildAgent"
|
|
26
|
+
description = "Build check before app run"
|
|
27
|
+
timeout = 120
|
|
28
|
+
|
|
29
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
30
|
+
root = inp.root
|
|
31
|
+
findings = []
|
|
32
|
+
logs = []
|
|
33
|
+
cmds = []
|
|
34
|
+
if (root / "package.json").exists():
|
|
35
|
+
try:
|
|
36
|
+
import json
|
|
37
|
+
|
|
38
|
+
pkg = json.loads((root / "package.json").read_text(encoding="utf-8"))
|
|
39
|
+
if "build" in pkg.get("scripts", {}):
|
|
40
|
+
cmds.append((["npm", "run", "build"], "package.json"))
|
|
41
|
+
if (root / "tsconfig.json").exists() and shutil.which("npx"):
|
|
42
|
+
cmds.append((["npx", "tsc", "--noEmit"], "tsconfig.json"))
|
|
43
|
+
except Exception as exc: # noqa: BLE001
|
|
44
|
+
_log.debug("build pkg read failed: %s", exc)
|
|
45
|
+
if (root / "Cargo.toml").exists() and shutil.which("cargo"):
|
|
46
|
+
cmds.append((["cargo", "check"], "Cargo.toml"))
|
|
47
|
+
for cmd, file in cmds:
|
|
48
|
+
if not shutil.which(cmd[0]):
|
|
49
|
+
continue
|
|
50
|
+
try:
|
|
51
|
+
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=60, cwd=str(root))
|
|
52
|
+
logs.append(f"$ {' '.join(cmd)}\n{proc.stdout[:500]}{proc.stderr[:500]}")
|
|
53
|
+
if proc.returncode != 0:
|
|
54
|
+
findings.append(make_finding(severity=Severity.HIGH, file=file, line_start=0, title=f"Build failed: {' '.join(cmd)}", description=(proc.stdout + proc.stderr)[:600], finding_type="build_failed"))
|
|
55
|
+
except Exception as exc: # noqa: BLE001
|
|
56
|
+
_log.debug("build %s failed: %s", cmd, exc)
|
|
57
|
+
result.status = AgentStatus.SUCCEEDED
|
|
58
|
+
result.findings = findings
|
|
59
|
+
result.data["build_logs"] = logs[:4]
|
|
60
|
+
out = root / ".patchi" / "launcher" / "build.log"
|
|
61
|
+
out.parent.mkdir(parents=True, exist_ok=True)
|
|
62
|
+
try:
|
|
63
|
+
out.write_text("\n".join(logs)[:8000], encoding="utf-8")
|
|
64
|
+
except OSError:
|
|
65
|
+
pass
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""FormatAgent §p check — ruff / eslint / prettier --check."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
|
|
9
|
+
from patchi.core.agents.base import (
|
|
10
|
+
AgentGroup,
|
|
11
|
+
AgentInput,
|
|
12
|
+
AgentResult,
|
|
13
|
+
AgentStatus,
|
|
14
|
+
BaseAgent,
|
|
15
|
+
Severity,
|
|
16
|
+
make_finding,
|
|
17
|
+
register,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
_log = logging.getLogger("patchi.agents.side.format")
|
|
21
|
+
|
|
22
|
+
@register
|
|
23
|
+
class FormatAgent(BaseAgent):
|
|
24
|
+
group = AgentGroup.SCANNER
|
|
25
|
+
name = "FormatAgent"
|
|
26
|
+
description = "Format check before app run"
|
|
27
|
+
timeout = 60
|
|
28
|
+
|
|
29
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
30
|
+
root = inp.root
|
|
31
|
+
findings = []
|
|
32
|
+
checks = [
|
|
33
|
+
(["ruff", "check", "."], "ruff"),
|
|
34
|
+
(["npx", "eslint", ".", "--max-warnings", "0"], "eslint"),
|
|
35
|
+
(["npx", "prettier", "--check", "."], "prettier"),
|
|
36
|
+
]
|
|
37
|
+
for cmd, name in checks:
|
|
38
|
+
if not shutil.which(cmd[0]):
|
|
39
|
+
continue
|
|
40
|
+
try:
|
|
41
|
+
proc = subprocess.run(cmd, capture_output=True, text=True, timeout=30, cwd=str(root))
|
|
42
|
+
if proc.returncode != 0 and proc.stdout.strip():
|
|
43
|
+
# only first 3 lines to avoid flood
|
|
44
|
+
snippet = "\n".join(proc.stdout.splitlines()[:3])[:400]
|
|
45
|
+
findings.append(make_finding(severity=Severity.LOW, file="", line_start=0, title=f"Format: {name} failed", description=snippet, finding_type="format_error"))
|
|
46
|
+
break # one format failure enough
|
|
47
|
+
except Exception as exc: # noqa: BLE001
|
|
48
|
+
_log.debug("format %s failed: %s", name, exc)
|
|
49
|
+
result.status = AgentStatus.SUCCEEDED
|
|
50
|
+
result.findings = findings
|