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,410 @@
|
|
|
1
|
+
"""
|
|
2
|
+
EnvScanner — secret/credential pattern detection.
|
|
3
|
+
|
|
4
|
+
Scans for potentially sensitive information in:
|
|
5
|
+
- Environment files (.env, .env.local, .env.production, etc.)
|
|
6
|
+
- Configuration files (json, yaml, toml)
|
|
7
|
+
- Source code (hardcoded secrets, API keys, passwords)
|
|
8
|
+
- Build files and scripts
|
|
9
|
+
|
|
10
|
+
Looks for patterns like:
|
|
11
|
+
- AWS keys (AKIA..., AGPA..., AIDA..., etc.)
|
|
12
|
+
- GitHub tokens (ghp_, gho_, ghu_, ghs_, ghr_)
|
|
13
|
+
- Slack tokens (xox[a-z]-...)
|
|
14
|
+
- Password fields in configs
|
|
15
|
+
- Database URLs with credentials
|
|
16
|
+
- Private key headers
|
|
17
|
+
|
|
18
|
+
Does NOT call AI.
|
|
19
|
+
Does NOT write to disk.
|
|
20
|
+
Does NOT touch the queue.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import logging
|
|
26
|
+
import re
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
|
|
29
|
+
from patchi.core.brain.languages import DEFAULT_IGNORE_DIRS
|
|
30
|
+
|
|
31
|
+
from .base import (
|
|
32
|
+
AgentGroup,
|
|
33
|
+
AgentInput,
|
|
34
|
+
AgentResult,
|
|
35
|
+
AgentStatus,
|
|
36
|
+
BaseAgent,
|
|
37
|
+
Finding,
|
|
38
|
+
Severity,
|
|
39
|
+
make_finding,
|
|
40
|
+
register,
|
|
41
|
+
safe_rglob,
|
|
42
|
+
scope_allows,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Directories holding intentional fake secrets (attacker fixtures, test
|
|
46
|
+
# data) — scanning them only produces false positives.
|
|
47
|
+
_FIXTURE_DIRS = ("attack_scenarios/", "tests/fixtures/", "fixtures/")
|
|
48
|
+
|
|
49
|
+
_log = logging.getLogger("patchi.agents.env_scanner")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@register
|
|
53
|
+
class EnvScanner(BaseAgent):
|
|
54
|
+
"""Scanner for environment variables and secrets."""
|
|
55
|
+
|
|
56
|
+
group = AgentGroup.SCANNER
|
|
57
|
+
name = "EnvScanner"
|
|
58
|
+
description = "Secret/credential pattern detection"
|
|
59
|
+
|
|
60
|
+
# Common secret patterns
|
|
61
|
+
SECRET_PATTERNS = [
|
|
62
|
+
# AWS keys
|
|
63
|
+
(r"AKIA[0-9A-Z]{16}", "AWS Access Key"),
|
|
64
|
+
(r"AGPA[0-9A-Z]{16}", "AWS Access Key"),
|
|
65
|
+
(r"AIDA[0-9A-Z]{16}", "AWS Access Key"),
|
|
66
|
+
(r"AROA[0-9A-Z]{16}", "AWS Access Key"),
|
|
67
|
+
(r"ASIA[0-9A-Z]{16}", "AWS Access Key"),
|
|
68
|
+
# AWS secret keys
|
|
69
|
+
(r"[0-9a-zA-Z/+]{40}(?=\s|$)", "AWS Secret Key"),
|
|
70
|
+
# GitHub tokens
|
|
71
|
+
(r"ghp_[0-9a-zA-Z]{36}", "GitHub Personal Access Token"),
|
|
72
|
+
(r"gho_[0-9a-zA-Z]{36}", "GitHub OAuth Access Token"),
|
|
73
|
+
(r"ghu_[0-9a-zA-Z]{36}", "GitHub User-to-server Token"),
|
|
74
|
+
(r"ghs_[0-9a-zA-Z]{36}", "GitHub Server-to-Server Token"),
|
|
75
|
+
(r"ghr_[0-9a-zA-Z]{76}", "GitHub Refresh Token"),
|
|
76
|
+
# Slack tokens
|
|
77
|
+
(r"xox[a-zA-Z]-[0-9A-Za-z-]+", "Slack Token"),
|
|
78
|
+
# Slack webhooks
|
|
79
|
+
(
|
|
80
|
+
r"https://hooks\.slack\.com/services/T[A-Z0-9]{8}/B[A-Z0-9]{8}/[a-zA-Z0-9]{24}",
|
|
81
|
+
"Slack Webhook",
|
|
82
|
+
),
|
|
83
|
+
# Google API keys
|
|
84
|
+
(r"AIza[0-9A-Za-z\\-_]{35}", "Google API Key"),
|
|
85
|
+
# Google OAuth
|
|
86
|
+
(r"[0-9]+-[0-9A-Za-z_]{32}\.apps\.googleusercontent\.com", "Google OAuth Client ID"),
|
|
87
|
+
# Facebook access tokens
|
|
88
|
+
(r"EAACEdEose0cBA[0-9A-Za-z]+", "Facebook Access Token"),
|
|
89
|
+
# Passwords in various formats
|
|
90
|
+
(r'password\s*[=:]\s*["\'][^"\']+["\']', "Password in config"),
|
|
91
|
+
(r'pwd\s*[=:]\s*["\'][^"\']+["\']', "Password in config"),
|
|
92
|
+
(r'passwd\s*[=:]\s*["\'][^"\']+["\']', "Password in config"),
|
|
93
|
+
# Database connection strings with credentials
|
|
94
|
+
(r"postgres://[a-zA-Z0-9_%]+:[^@]+@", "PostgreSQL Connection String"),
|
|
95
|
+
(r"mysql://[a-zA-Z0-9_%]+:[^@]+@", "MySQL Connection String"),
|
|
96
|
+
(r"mongodb://[a-zA-Z0-9_%]+:[^@]+@", "MongoDB Connection String"),
|
|
97
|
+
# Private key headers
|
|
98
|
+
(r"-----BEGIN RSA PRIVATE KEY-----", "RSA Private Key"),
|
|
99
|
+
(r"-----BEGIN DSA PRIVATE KEY-----", "DSA Private Key"),
|
|
100
|
+
(r"-----BEGIN EC PRIVATE KEY-----", "EC Private Key"),
|
|
101
|
+
(r"-----BEGIN OPENSSH PRIVATE KEY-----", "OpenSSH Private Key"),
|
|
102
|
+
(r"-----BEGIN PRIVATE KEY-----", "Private Key"),
|
|
103
|
+
# SSH keys
|
|
104
|
+
(r"ssh-rsa [A-Za-z0-9+/\s]+={0,2}", "SSH Public Key"),
|
|
105
|
+
(r"ssh-ed25519 [A-Za-z0-9+/\s]+={0,2}", "SSH Ed25519 Key"),
|
|
106
|
+
# Generic API keys
|
|
107
|
+
(r'api[_-]?key["\']?\s*[=:]\s*["\'][^"\']+["\']', "Generic API Key"),
|
|
108
|
+
(r'api[_-]?token["\']?\s*[=:]\s*["\'][^"\']+["\']', "Generic API Token"),
|
|
109
|
+
(r'secret["\']?\s*[=:]\s*["\'][^"\']+["\']', "Generic Secret"),
|
|
110
|
+
(r'token["\']?\s*[=:]\s*["\'][^"\']+["\']', "Generic Token"),
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
114
|
+
"""Scan for environment variables and secrets."""
|
|
115
|
+
findings = []
|
|
116
|
+
|
|
117
|
+
# Define environment file patterns
|
|
118
|
+
env_patterns = [
|
|
119
|
+
".env",
|
|
120
|
+
".env.*",
|
|
121
|
+
".envrc",
|
|
122
|
+
"*.env",
|
|
123
|
+
"*.config",
|
|
124
|
+
"*.conf",
|
|
125
|
+
"*.ini",
|
|
126
|
+
"*.properties",
|
|
127
|
+
"*.json",
|
|
128
|
+
"*.yaml",
|
|
129
|
+
"*.yml",
|
|
130
|
+
"*.toml",
|
|
131
|
+
"*.py",
|
|
132
|
+
"*.js",
|
|
133
|
+
"*.ts",
|
|
134
|
+
"*.jsx",
|
|
135
|
+
"*.tsx",
|
|
136
|
+
"*.sh",
|
|
137
|
+
"*.bash",
|
|
138
|
+
"*.zsh",
|
|
139
|
+
"*.ps1",
|
|
140
|
+
"*.bat",
|
|
141
|
+
"Dockerfile*",
|
|
142
|
+
"docker-compose*.yml",
|
|
143
|
+
"docker-compose*.yaml",
|
|
144
|
+
"package.json",
|
|
145
|
+
"requirements.txt",
|
|
146
|
+
"Pipfile",
|
|
147
|
+
"Gemfile",
|
|
148
|
+
"composer.json",
|
|
149
|
+
]
|
|
150
|
+
|
|
151
|
+
# Search for environment/config files
|
|
152
|
+
for pattern in env_patterns:
|
|
153
|
+
for file_path in safe_rglob(inp.root, pattern):
|
|
154
|
+
if file_path.is_file():
|
|
155
|
+
rel_path = file_path.relative_to(inp.root).as_posix()
|
|
156
|
+
if not scope_allows(inp, rel_path):
|
|
157
|
+
continue
|
|
158
|
+
if not self._should_skip_file(rel_path, inp):
|
|
159
|
+
findings.extend(self._scan_env_file(file_path, rel_path))
|
|
160
|
+
|
|
161
|
+
result.status = AgentStatus.SUCCEEDED
|
|
162
|
+
result.findings = findings
|
|
163
|
+
result.data.update(
|
|
164
|
+
{
|
|
165
|
+
"secrets_found": len(
|
|
166
|
+
[
|
|
167
|
+
f
|
|
168
|
+
for f in findings
|
|
169
|
+
if "Secret" in f.title or "Token" in f.title or "Key" in f.title
|
|
170
|
+
]
|
|
171
|
+
),
|
|
172
|
+
"passwords_found": len([f for f in findings if "Password" in f.title]),
|
|
173
|
+
"needs_ai": False,
|
|
174
|
+
}
|
|
175
|
+
)
|
|
176
|
+
return
|
|
177
|
+
|
|
178
|
+
def _should_skip_file(self, file_path: str, inp: AgentInput) -> bool:
|
|
179
|
+
"""Check if file should be skipped based on restrictions."""
|
|
180
|
+
# Skip ignored directories (node_modules, .venv, etc.)
|
|
181
|
+
from pathlib import PurePosixPath
|
|
182
|
+
|
|
183
|
+
if any(p in DEFAULT_IGNORE_DIRS for p in PurePosixPath(file_path).parts):
|
|
184
|
+
return True
|
|
185
|
+
|
|
186
|
+
# Skip intentional-secret fixtures (attacker scenarios, test data),
|
|
187
|
+
# wherever nested in the tree
|
|
188
|
+
parts = PurePosixPath(file_path).parts
|
|
189
|
+
if any(d.strip("/") in parts for d in _FIXTURE_DIRS):
|
|
190
|
+
return True
|
|
191
|
+
|
|
192
|
+
from pathlib import PurePosixPath
|
|
193
|
+
|
|
194
|
+
# Check restrictions
|
|
195
|
+
restrictions = inp.config.get("restrictions", [])
|
|
196
|
+
for r in restrictions:
|
|
197
|
+
if r.get("enabled", True):
|
|
198
|
+
path = r["path"]
|
|
199
|
+
if file_path.startswith(path) or PurePosixPath(file_path).match(path):
|
|
200
|
+
if r["type"] == "NO_TOUCH":
|
|
201
|
+
return True
|
|
202
|
+
elif r["type"] == "SCAN_ONLY" and self.__class__.__name__ == "FixAgent":
|
|
203
|
+
return True
|
|
204
|
+
return False
|
|
205
|
+
|
|
206
|
+
def _scan_env_file(self, file_path: Path, rel_path: str) -> list[Finding]:
|
|
207
|
+
"""Scan an environment/config file for secrets."""
|
|
208
|
+
findings = []
|
|
209
|
+
|
|
210
|
+
if file_path.name == "package-lock.json":
|
|
211
|
+
return findings
|
|
212
|
+
|
|
213
|
+
try:
|
|
214
|
+
content = file_path.read_text(encoding="utf-8")
|
|
215
|
+
lines = content.splitlines()
|
|
216
|
+
|
|
217
|
+
# Check each line for secret patterns
|
|
218
|
+
for line_num, line in enumerate(lines, 1):
|
|
219
|
+
stripped = line.strip()
|
|
220
|
+
# Skip comment lines and pattern/regex definitions (not real secrets)
|
|
221
|
+
if stripped.startswith(("#", "//", "*", "/*")):
|
|
222
|
+
continue
|
|
223
|
+
_fp_markers = (
|
|
224
|
+
"re.compile",
|
|
225
|
+
"re.search",
|
|
226
|
+
"re.match",
|
|
227
|
+
"re.finditer",
|
|
228
|
+
"SECRET_PATTERNS",
|
|
229
|
+
"_PATTERN",
|
|
230
|
+
"REGEX",
|
|
231
|
+
"RE_",
|
|
232
|
+
)
|
|
233
|
+
# Self-detection guard: regex pattern strings that LOOK like
|
|
234
|
+
# secrets (e.g. "-----BEGIN RSA PRIVATE KEY-----" inside
|
|
235
|
+
# this file) must not be flagged.
|
|
236
|
+
if "PRIVATE KEY" in line and ('r"' in line or "r'" in line):
|
|
237
|
+
continue
|
|
238
|
+
if any(m in line for m in _fp_markers):
|
|
239
|
+
continue
|
|
240
|
+
for pattern, description in self.SECRET_PATTERNS:
|
|
241
|
+
matches = re.finditer(pattern, line, re.IGNORECASE)
|
|
242
|
+
for match in matches:
|
|
243
|
+
masked_match = self._mask_sensitive_info(match.group(0))
|
|
244
|
+
findings.append(
|
|
245
|
+
make_finding(
|
|
246
|
+
severity=Severity.CRITICAL,
|
|
247
|
+
finding_type="hardcoded_secret",
|
|
248
|
+
file=rel_path,
|
|
249
|
+
line_start=line_num,
|
|
250
|
+
title=f"Potential {description}",
|
|
251
|
+
description=f"Found potential {description} in {file_path.name}",
|
|
252
|
+
evidence=f"Match: {masked_match}",
|
|
253
|
+
)
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
# Special check for .env files - look for common environment variable patterns
|
|
257
|
+
if ".env" in file_path.name.lower():
|
|
258
|
+
findings.extend(self._scan_dotenv_file(file_path, rel_path, lines))
|
|
259
|
+
|
|
260
|
+
except UnicodeDecodeError:
|
|
261
|
+
# If it's a binary file, try to read as binary and decode what we can
|
|
262
|
+
try:
|
|
263
|
+
content = file_path.read_bytes()
|
|
264
|
+
# Try to decode as UTF-8, ignoring errors
|
|
265
|
+
text_content = content.decode("utf-8", errors="ignore")
|
|
266
|
+
lines = text_content.splitlines()
|
|
267
|
+
|
|
268
|
+
for line_num, line in enumerate(lines, 1):
|
|
269
|
+
for pattern, description in self.SECRET_PATTERNS:
|
|
270
|
+
matches = re.finditer(pattern, line, re.IGNORECASE)
|
|
271
|
+
for match in matches:
|
|
272
|
+
masked_match = self._mask_sensitive_info(match.group(0))
|
|
273
|
+
|
|
274
|
+
findings.append(
|
|
275
|
+
make_finding(
|
|
276
|
+
severity=Severity.CRITICAL,
|
|
277
|
+
finding_type="hardcoded_secret",
|
|
278
|
+
file=rel_path,
|
|
279
|
+
line_start=line_num,
|
|
280
|
+
title=f"Potential {description}",
|
|
281
|
+
description=f"Found potential {description} in {file_path.name}",
|
|
282
|
+
evidence=f"Match: {masked_match}",
|
|
283
|
+
)
|
|
284
|
+
)
|
|
285
|
+
except Exception as e:
|
|
286
|
+
_log.warning("EnvScanner._scan_env_file failed: %s", e)
|
|
287
|
+
findings.append(
|
|
288
|
+
make_finding(
|
|
289
|
+
severity=Severity.LOW,
|
|
290
|
+
file=rel_path,
|
|
291
|
+
line_start=0,
|
|
292
|
+
title="File read error",
|
|
293
|
+
description=f"Could not read {file_path.name} for environment variable scanning",
|
|
294
|
+
evidence="Binary or corrupted file",
|
|
295
|
+
)
|
|
296
|
+
)
|
|
297
|
+
except Exception as e:
|
|
298
|
+
findings.append(
|
|
299
|
+
make_finding(
|
|
300
|
+
severity=Severity.LOW,
|
|
301
|
+
file=rel_path,
|
|
302
|
+
line_start=0,
|
|
303
|
+
title="Environment file read error",
|
|
304
|
+
description=f"Could not read environment file {file_path.name}: {str(e)}",
|
|
305
|
+
evidence=str(e),
|
|
306
|
+
)
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
return findings
|
|
310
|
+
|
|
311
|
+
def _scan_dotenv_file(self, file_path: Path, rel_path: str, lines: list[str]) -> list[Finding]:
|
|
312
|
+
"""Special scanning for .env files."""
|
|
313
|
+
findings = []
|
|
314
|
+
|
|
315
|
+
# Look for common environment variable patterns that might contain secrets
|
|
316
|
+
for line_num, line in enumerate(lines, 1):
|
|
317
|
+
# Skip comments and empty lines
|
|
318
|
+
if line.strip().startswith("#") or not line.strip():
|
|
319
|
+
continue
|
|
320
|
+
|
|
321
|
+
# Look for assignment patterns
|
|
322
|
+
if "=" in line:
|
|
323
|
+
var_name, var_value = line.split("=", 1)
|
|
324
|
+
var_name = var_name.strip()
|
|
325
|
+
var_value = var_value.strip().strip("\"'") # Remove quotes
|
|
326
|
+
|
|
327
|
+
# Check if the variable name suggests it might be sensitive
|
|
328
|
+
sensitive_keywords = [
|
|
329
|
+
"KEY",
|
|
330
|
+
"SECRET",
|
|
331
|
+
"TOKEN",
|
|
332
|
+
"PASSWORD",
|
|
333
|
+
"PWD",
|
|
334
|
+
"PASSPHRASE",
|
|
335
|
+
"API",
|
|
336
|
+
"AUTH",
|
|
337
|
+
"CREDENTIAL",
|
|
338
|
+
"ACCESS",
|
|
339
|
+
"CLIENT_ID",
|
|
340
|
+
"CLIENT_SECRET",
|
|
341
|
+
"DATABASE_URL",
|
|
342
|
+
"DB_URL",
|
|
343
|
+
"REDIS_URL",
|
|
344
|
+
"MONGO_URL",
|
|
345
|
+
]
|
|
346
|
+
|
|
347
|
+
if any(keyword in var_name.upper() for keyword in sensitive_keywords):
|
|
348
|
+
# Check if the value looks like a secret
|
|
349
|
+
if self._looks_like_secret(var_value):
|
|
350
|
+
findings.append(
|
|
351
|
+
make_finding(
|
|
352
|
+
severity=Severity.HIGH,
|
|
353
|
+
finding_type="hardcoded_secret",
|
|
354
|
+
file=rel_path,
|
|
355
|
+
line_start=line_num,
|
|
356
|
+
title=f"Potential secret in environment variable: {var_name}",
|
|
357
|
+
description=f"Environment variable '{var_name}' may contain sensitive information",
|
|
358
|
+
evidence=f"Variable: {var_name}, Value (masked): {self._mask_sensitive_info(var_value)}",
|
|
359
|
+
)
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
return findings
|
|
363
|
+
|
|
364
|
+
def _looks_like_secret(self, value: str) -> bool:
|
|
365
|
+
"""Check if a value looks like it might be a secret."""
|
|
366
|
+
# Check for common secret characteristics
|
|
367
|
+
if len(value) < 6: # Too short to be a secret
|
|
368
|
+
return False
|
|
369
|
+
|
|
370
|
+
# Check for randomness - if it has high entropy it might be a secret
|
|
371
|
+
if self._has_high_entropy(value):
|
|
372
|
+
return True
|
|
373
|
+
|
|
374
|
+
# Check against our known patterns
|
|
375
|
+
for pattern, _ in self.SECRET_PATTERNS:
|
|
376
|
+
if re.search(pattern, value, re.IGNORECASE):
|
|
377
|
+
return True
|
|
378
|
+
|
|
379
|
+
return False
|
|
380
|
+
|
|
381
|
+
def _has_high_entropy(self, text: str) -> bool:
|
|
382
|
+
"""Check if text has high entropy (suggesting it might be a key/token)."""
|
|
383
|
+
if len(text) < 10: # Too short to reliably determine entropy
|
|
384
|
+
return False
|
|
385
|
+
|
|
386
|
+
# Calculate character diversity
|
|
387
|
+
unique_chars = len(set(text))
|
|
388
|
+
total_chars = len(text)
|
|
389
|
+
|
|
390
|
+
# If more than 50% of characters are unique, it might be high entropy
|
|
391
|
+
if unique_chars / total_chars > 0.5:
|
|
392
|
+
return True
|
|
393
|
+
|
|
394
|
+
return False
|
|
395
|
+
|
|
396
|
+
def _mask_sensitive_info(self, text: str) -> str:
|
|
397
|
+
"""Mask sensitive information for display."""
|
|
398
|
+
if not text:
|
|
399
|
+
return ""
|
|
400
|
+
|
|
401
|
+
# For API keys and tokens, show first 4 and last 4 characters
|
|
402
|
+
if len(text) > 8:
|
|
403
|
+
return f"{text[:4]}...{text[-4:]}"
|
|
404
|
+
|
|
405
|
+
# For shorter texts, mask the middle
|
|
406
|
+
if len(text) > 4:
|
|
407
|
+
return f"{text[:2]}...{text[-2:]}"
|
|
408
|
+
|
|
409
|
+
# For very short texts, mask completely
|
|
410
|
+
return "***"
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FeatureFlagArchaeology §2.1.4 — hardcoded boolean flags for >N releases.
|
|
3
|
+
|
|
4
|
+
Detects `if (FEATURE_X)`, `featureFlag === true` where flag value is constant
|
|
5
|
+
across N commits (git log + blame). Flags: true/false, 0/1, "on"/"off".
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import logging
|
|
11
|
+
import re
|
|
12
|
+
import subprocess
|
|
13
|
+
from pathlib import Path
|
|
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.feature_flag")
|
|
28
|
+
|
|
29
|
+
_FLAG_RE = re.compile(r"(?:feature[_-]?flag|FLAG|ff[_A-Z]+|isEnabled|enabled)\s*[=:]\s*(true|false|1|0|\"on\"|\'on\'|\"off\"|\'off\')", re.I)
|
|
30
|
+
_BOOL_ASSIGN = re.compile(r"^\s*(?:const|let|var)?\s*(\w*(?:flag|enabled|feature)\w*)\s*=\s*(true|false)\b", re.I | re.M)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _git_log_count(root: Path, rel: str, flag_name: str, commits: int = 20) -> int:
|
|
34
|
+
"""How many recent commits keep the same flag value line."""
|
|
35
|
+
try:
|
|
36
|
+
out = subprocess.run(
|
|
37
|
+
["git", "log", f"-{commits}", "--oneline", "--", rel],
|
|
38
|
+
capture_output=True,
|
|
39
|
+
text=True,
|
|
40
|
+
timeout=5,
|
|
41
|
+
cwd=str(root),
|
|
42
|
+
)
|
|
43
|
+
if out.returncode != 0:
|
|
44
|
+
return 0
|
|
45
|
+
return len([line for line in out.stdout.splitlines() if line.strip()])
|
|
46
|
+
except Exception as exc: # noqa: BLE001
|
|
47
|
+
_log.debug("git log %s failed: %s", flag_name, exc)
|
|
48
|
+
return 0
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@register
|
|
52
|
+
class FeatureFlagArchaeologyAgent(BaseAgent):
|
|
53
|
+
group = AgentGroup.SCANNER
|
|
54
|
+
name = "FeatureFlagArchaeologyAgent"
|
|
55
|
+
description = "Hardcoded boolean flags (feature flag archaeology) — §2.1.4"
|
|
56
|
+
timeout = 60
|
|
57
|
+
|
|
58
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
59
|
+
findings = []
|
|
60
|
+
for pattern in ("*.py", "*.js", "*.ts", "*.tsx", "*.go", "*.java"):
|
|
61
|
+
for fp in safe_rglob(inp.root, pattern):
|
|
62
|
+
if not fp.is_file():
|
|
63
|
+
continue
|
|
64
|
+
rel = fp.relative_to(inp.root).as_posix()
|
|
65
|
+
try:
|
|
66
|
+
txt = fp.read_text(encoding="utf-8", errors="replace")
|
|
67
|
+
except OSError:
|
|
68
|
+
continue
|
|
69
|
+
for m in _BOOL_ASSIGN.finditer(txt):
|
|
70
|
+
name = m.group(1)
|
|
71
|
+
val = m.group(2)
|
|
72
|
+
line = txt[: m.start()].count("\n") + 1
|
|
73
|
+
# Check git history depth for this flag line's file
|
|
74
|
+
depth = _git_log_count(inp.root, rel, name, commits=15)
|
|
75
|
+
if depth >= 10: # hardcoded for >10 releases
|
|
76
|
+
findings.append(
|
|
77
|
+
make_finding(
|
|
78
|
+
severity=Severity.LOW,
|
|
79
|
+
file=rel,
|
|
80
|
+
line_start=line,
|
|
81
|
+
title=f"Hardcoded flag {name}={val} for {depth} commits (archaeology)",
|
|
82
|
+
description=f"Boolean flag '{name}' has been {val} for {depth} recent commits touching {rel} — likely dead code. Remove flag or make dynamic. References: uber/piranha for refactoring.",
|
|
83
|
+
evidence=m.group(0).strip()[:120],
|
|
84
|
+
finding_type="hardcoded_flag",
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
result.status = AgentStatus.SUCCEEDED
|
|
88
|
+
result.findings = findings[:50]
|
|
89
|
+
result.data["flag_count"] = len(findings)
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
"""
|
|
2
|
+
FrontendFrameworkAgent §8.1.1-5 — React/Vue/Svelte/Angular/Solid checks.
|
|
3
|
+
|
|
4
|
+
Uses ESLint plugins where available, else tree-sitter structural queries.
|
|
5
|
+
|
|
6
|
+
Checks:
|
|
7
|
+
React: hooks rules, missing key in list, useEffect dep array
|
|
8
|
+
Vue: v-for key, template type, composition API
|
|
9
|
+
Svelte: store subscription leak ($store without unsubscribe)
|
|
10
|
+
Angular: DI, template type, standalone
|
|
11
|
+
Solid: createEffect without cleanup
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
|
|
18
|
+
from patchi.core.agents.base import (
|
|
19
|
+
AgentGroup,
|
|
20
|
+
AgentInput,
|
|
21
|
+
AgentResult,
|
|
22
|
+
AgentStatus,
|
|
23
|
+
BaseAgent,
|
|
24
|
+
Severity,
|
|
25
|
+
make_finding,
|
|
26
|
+
register,
|
|
27
|
+
safe_rglob,
|
|
28
|
+
)
|
|
29
|
+
from patchi.core.brain.code_query import (
|
|
30
|
+
extract_script_blocks,
|
|
31
|
+
js_call_names,
|
|
32
|
+
js_calls,
|
|
33
|
+
js_constructor_di_line,
|
|
34
|
+
js_identifier_lines,
|
|
35
|
+
js_jsx_attributes,
|
|
36
|
+
js_jsx_elements,
|
|
37
|
+
lang_for_file,
|
|
38
|
+
parse_js,
|
|
39
|
+
vue_template_attrs,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
_log = logging.getLogger("patchi.agents.frontend_framework")
|
|
43
|
+
|
|
44
|
+
_HOOKS = {"useEffect", "useState", "useMemo", "useCallback", "useRef"}
|
|
45
|
+
|
|
46
|
+
@register
|
|
47
|
+
class FrontendFrameworkAgent(BaseAgent):
|
|
48
|
+
group = AgentGroup.SCANNER
|
|
49
|
+
name = "FrontendFrameworkAgent"
|
|
50
|
+
description = "React/Vue/Svelte/Angular/Solid framework checks §8.1.1-5"
|
|
51
|
+
timeout = 60
|
|
52
|
+
|
|
53
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
54
|
+
findings=[]
|
|
55
|
+
for pat in ("*.jsx","*.tsx","*.js","*.ts","*.vue","*.svelte"):
|
|
56
|
+
for fp in safe_rglob(inp.root, pat):
|
|
57
|
+
rel=fp.relative_to(inp.root).as_posix()
|
|
58
|
+
if "node_modules" in rel or "tests" in rel:
|
|
59
|
+
continue
|
|
60
|
+
try:
|
|
61
|
+
txt=fp.read_text(encoding="utf-8", errors="replace")
|
|
62
|
+
except OSError:
|
|
63
|
+
continue
|
|
64
|
+
lang = lang_for_file(rel)
|
|
65
|
+
tree = parse_js(txt, lang)
|
|
66
|
+
if tree is None:
|
|
67
|
+
continue
|
|
68
|
+
calls = js_calls(tree, lang)
|
|
69
|
+
names = {c.name for c in calls}
|
|
70
|
+
is_jsx = fp.suffix in (".jsx", ".tsx") or "React" in txt
|
|
71
|
+
# React: useEffect without dep array (single-argument call)
|
|
72
|
+
if is_jsx:
|
|
73
|
+
for call in calls:
|
|
74
|
+
if call.name == "useEffect" and len(call.arg_kinds) < 2:
|
|
75
|
+
findings.append(make_finding(severity=Severity.LOW, file=rel, line_start=call.line, title="React useEffect missing deps", description="Add dependency array or disable exhaustive-deps consciously", finding_type="react_hook"))
|
|
76
|
+
break
|
|
77
|
+
# React: list rendering without key (previously dead branch)
|
|
78
|
+
attrs = js_jsx_attributes(tree, lang)
|
|
79
|
+
if any(c.name in ("map", "forEach") for c in calls):
|
|
80
|
+
if not any(name == "key" for _, name, _ in attrs):
|
|
81
|
+
elems = js_jsx_elements(tree, lang)
|
|
82
|
+
line0 = elems[0][1] if elems else 1
|
|
83
|
+
findings.append(make_finding(severity=Severity.LOW, file=rel, line_start=line0, title="List element without key", description="Add key to list-rendered elements for stable diffing", finding_type="react_key"))
|
|
84
|
+
# Vue: v-for without :key (template markup via html.parser)
|
|
85
|
+
if fp.suffix == ".vue":
|
|
86
|
+
by_el: dict[tuple[str, int], set[str]] = {}
|
|
87
|
+
for tag, attr, line in vue_template_attrs(txt):
|
|
88
|
+
by_el.setdefault((tag, line), set()).add(attr)
|
|
89
|
+
for (_tag, line), attr_set in by_el.items():
|
|
90
|
+
if "v-for" in attr_set and "key" not in attr_set and ":key" not in attr_set:
|
|
91
|
+
findings.append(make_finding(severity=Severity.LOW, file=rel, line_start=line, title="Vue v-for without :key", description="Add :key to v-for for stable diffing", finding_type="vue_key"))
|
|
92
|
+
break
|
|
93
|
+
# Svelte: $store subscription without unsubscribe
|
|
94
|
+
if fp.suffix == ".svelte":
|
|
95
|
+
for block in extract_script_blocks(txt) or [txt]:
|
|
96
|
+
stree = parse_js(block, "javascript")
|
|
97
|
+
if stree is None:
|
|
98
|
+
continue
|
|
99
|
+
snames = js_call_names(stree, "javascript")
|
|
100
|
+
stores = [
|
|
101
|
+
(t, n)
|
|
102
|
+
for t, n in js_identifier_lines(stree, "javascript")
|
|
103
|
+
if t.startswith("$")
|
|
104
|
+
]
|
|
105
|
+
if not stores or "subscribe" not in snames:
|
|
106
|
+
continue
|
|
107
|
+
if snames & {"unsubscribe", "onDestroy"}:
|
|
108
|
+
continue
|
|
109
|
+
findings.append(make_finding(
|
|
110
|
+
severity=Severity.LOW, file=rel, line_start=stores[0][1],
|
|
111
|
+
title="Svelte store without unsubscribe",
|
|
112
|
+
description=(
|
|
113
|
+
"Store subscription may leak; "
|
|
114
|
+
"use $store auto-sub or onDestroy unsubscribe"
|
|
115
|
+
),
|
|
116
|
+
finding_type="svelte_store_leak",
|
|
117
|
+
))
|
|
118
|
+
break
|
|
119
|
+
# Angular: constructor DI (private param)
|
|
120
|
+
if "Angular" in txt or "@Component" in txt:
|
|
121
|
+
line = js_constructor_di_line(tree, lang)
|
|
122
|
+
if line:
|
|
123
|
+
findings.append(make_finding(severity=Severity.INFO, file=rel, line_start=line, title="Angular DI injection", description="Verify DI token provided", finding_type="angular_di"))
|
|
124
|
+
# Solid: createEffect without cleanup
|
|
125
|
+
if "createEffect" in names:
|
|
126
|
+
line = next(c.line for c in calls if c.name == "createEffect")
|
|
127
|
+
findings.append(make_finding(severity=Severity.INFO, file=rel, line_start=line, title="Solid createEffect without cleanup", description="Return cleanup function if needed", finding_type="solid_effect"))
|
|
128
|
+
if len(findings) >= 40:
|
|
129
|
+
break
|
|
130
|
+
if len(findings) >= 40:
|
|
131
|
+
break
|
|
132
|
+
result.status=AgentStatus.SUCCEEDED
|
|
133
|
+
result.findings=findings[:40]
|