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,602 @@
|
|
|
1
|
+
"""
|
|
2
|
+
AuthZAgent — authorization bypass, privilege escalation.
|
|
3
|
+
|
|
4
|
+
Detects authorization issues:
|
|
5
|
+
- Missing authentication checks
|
|
6
|
+
- Broken access control
|
|
7
|
+
- Privilege escalation vulnerabilities
|
|
8
|
+
- Horizontal/vertical privilege escalation
|
|
9
|
+
- Missing authorization middleware
|
|
10
|
+
- Overly permissive access controls
|
|
11
|
+
- Insecure direct object references (IDOR)
|
|
12
|
+
|
|
13
|
+
Uses pattern matching and AST analysis.
|
|
14
|
+
Does NOT call AI.
|
|
15
|
+
Does NOT write to disk.
|
|
16
|
+
Does NOT touch the queue.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import ast as py_ast
|
|
22
|
+
import logging
|
|
23
|
+
import re
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
from ..agents.base import (
|
|
27
|
+
AgentDomain,
|
|
28
|
+
AgentGroup,
|
|
29
|
+
AgentInput,
|
|
30
|
+
AgentResult,
|
|
31
|
+
AgentStatus,
|
|
32
|
+
BaseAgent,
|
|
33
|
+
Finding,
|
|
34
|
+
Severity,
|
|
35
|
+
make_finding,
|
|
36
|
+
register,
|
|
37
|
+
safe_rglob,
|
|
38
|
+
)
|
|
39
|
+
from ..brain.ast_utils import node_text, parse_source
|
|
40
|
+
from ..brain.languages import TREE_SITTER_LANGS, Lang, detect_language, get_parser
|
|
41
|
+
|
|
42
|
+
_log = logging.getLogger("patchi.security.authz_agent")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@register
|
|
46
|
+
class AuthZAgent(BaseAgent):
|
|
47
|
+
"""Agent for detecting authorization vulnerabilities."""
|
|
48
|
+
|
|
49
|
+
group = AgentGroup.SECURITY
|
|
50
|
+
domain = AgentDomain.SECURITY
|
|
51
|
+
name = "AuthZAgent"
|
|
52
|
+
description = "Authorization bypass, privilege escalation, IDOR, missing auth checks"
|
|
53
|
+
|
|
54
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
55
|
+
from patchi.core.brain.project_context import agent_is_relevant
|
|
56
|
+
|
|
57
|
+
relevant, reason = agent_is_relevant(inp.domain, inp.purpose, self.name, inp.active_domains)
|
|
58
|
+
if not relevant:
|
|
59
|
+
result.status = AgentStatus.SKIPPED
|
|
60
|
+
result.data.update({"skip_reason": reason, "needs_ai": False})
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
findings = []
|
|
64
|
+
|
|
65
|
+
# Define source file patterns to scan
|
|
66
|
+
source_patterns = [
|
|
67
|
+
"*.py",
|
|
68
|
+
"*.js",
|
|
69
|
+
"*.jsx",
|
|
70
|
+
"*.ts",
|
|
71
|
+
"*.tsx",
|
|
72
|
+
"*.java",
|
|
73
|
+
"*.php",
|
|
74
|
+
"*.rb",
|
|
75
|
+
"*.go",
|
|
76
|
+
"*.rs",
|
|
77
|
+
"*.cpp",
|
|
78
|
+
"*.cxx",
|
|
79
|
+
"*.cc",
|
|
80
|
+
"*.c",
|
|
81
|
+
"*.h",
|
|
82
|
+
"*.hpp",
|
|
83
|
+
"*.cs",
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
# Search for source files
|
|
87
|
+
for pattern in source_patterns:
|
|
88
|
+
for file_path in safe_rglob(inp.root, pattern):
|
|
89
|
+
if file_path.is_file():
|
|
90
|
+
rel_path = file_path.relative_to(inp.root).as_posix()
|
|
91
|
+
if not self._should_skip_file(rel_path, inp):
|
|
92
|
+
findings.extend(self._scan_file_authz(file_path, rel_path))
|
|
93
|
+
|
|
94
|
+
result.status = AgentStatus.SUCCEEDED
|
|
95
|
+
result.findings = findings
|
|
96
|
+
result.data.update(
|
|
97
|
+
{
|
|
98
|
+
"authz_findings": len(
|
|
99
|
+
[
|
|
100
|
+
f
|
|
101
|
+
for f in findings
|
|
102
|
+
if any(
|
|
103
|
+
word in f.title.lower()
|
|
104
|
+
for word in ["authorization", "auth", "privilege", "idor"]
|
|
105
|
+
)
|
|
106
|
+
]
|
|
107
|
+
),
|
|
108
|
+
"needs_ai": False,
|
|
109
|
+
}
|
|
110
|
+
)
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
def _should_skip_file(self, file_path: str, inp: AgentInput) -> bool:
|
|
114
|
+
"""Check if file should be skipped based on restrictions."""
|
|
115
|
+
from pathlib import PurePosixPath
|
|
116
|
+
|
|
117
|
+
# Check restrictions
|
|
118
|
+
restrictions = inp.config.get("restrictions", [])
|
|
119
|
+
for r in restrictions:
|
|
120
|
+
if r.get("enabled", True):
|
|
121
|
+
path = r["path"]
|
|
122
|
+
if file_path.startswith(path) or PurePosixPath(file_path).match(path):
|
|
123
|
+
if r["type"] == "NO_TOUCH":
|
|
124
|
+
return True
|
|
125
|
+
elif r["type"] == "SCAN_ONLY" and self.__class__.__name__ == "FixAgent":
|
|
126
|
+
return True
|
|
127
|
+
return False
|
|
128
|
+
|
|
129
|
+
def _scan_file_authz(self, file_path: Path, rel_path: str) -> list[Finding]:
|
|
130
|
+
"""Scan a file for authorization vulnerabilities."""
|
|
131
|
+
findings = []
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
content = file_path.read_text(encoding="utf-8")
|
|
135
|
+
lang = detect_language(file_path)
|
|
136
|
+
|
|
137
|
+
# Apply language-specific authorization checks
|
|
138
|
+
if lang == Lang.PYTHON:
|
|
139
|
+
findings.extend(self._scan_python_authz(content, rel_path))
|
|
140
|
+
elif lang in [Lang.JAVASCRIPT, Lang.TYPESCRIPT]:
|
|
141
|
+
findings.extend(self._scan_javascript_authz(content, rel_path, lang))
|
|
142
|
+
elif lang == Lang.JAVA:
|
|
143
|
+
findings.extend(self._scan_java_authz(content, rel_path))
|
|
144
|
+
elif lang == Lang.PHP:
|
|
145
|
+
findings.extend(self._scan_php_authz(content, rel_path))
|
|
146
|
+
elif lang == Lang.RUBY:
|
|
147
|
+
findings.extend(self._scan_ruby_authz(content, rel_path))
|
|
148
|
+
elif lang in TREE_SITTER_LANGS:
|
|
149
|
+
findings.extend(self._scan_treesitter_authz(content, rel_path, lang))
|
|
150
|
+
else:
|
|
151
|
+
# Apply general authorization checks
|
|
152
|
+
findings.extend(self._scan_general_authz(content, rel_path))
|
|
153
|
+
|
|
154
|
+
except Exception as e:
|
|
155
|
+
findings.append(
|
|
156
|
+
make_finding(
|
|
157
|
+
severity=Severity.LOW,
|
|
158
|
+
file=rel_path,
|
|
159
|
+
line_start=0,
|
|
160
|
+
title="AuthZ scanner file read error",
|
|
161
|
+
description=f"Could not analyze {file_path.name} for authorization issues: {str(e)}",
|
|
162
|
+
evidence=str(e),
|
|
163
|
+
)
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
return findings
|
|
167
|
+
|
|
168
|
+
def _scan_python_authz(self, content: str, rel_path: str) -> list[Finding]:
|
|
169
|
+
"""Scan Python code for authorization vulnerabilities."""
|
|
170
|
+
findings = []
|
|
171
|
+
|
|
172
|
+
try:
|
|
173
|
+
tree = py_ast.parse(content)
|
|
174
|
+
except SyntaxError:
|
|
175
|
+
# If we can't parse the AST, fall back to pattern matching
|
|
176
|
+
return self._scan_general_authz(content, rel_path)
|
|
177
|
+
|
|
178
|
+
# Walk the AST to find authorization issues
|
|
179
|
+
routes_with_auth = set()
|
|
180
|
+
routes_without_auth = set()
|
|
181
|
+
|
|
182
|
+
for node in py_ast.walk(tree):
|
|
183
|
+
# Look for route definitions in common frameworks
|
|
184
|
+
if isinstance(node, py_ast.FunctionDef):
|
|
185
|
+
func_name = node.name
|
|
186
|
+
has_auth_decorator = False
|
|
187
|
+
|
|
188
|
+
# Check for authentication/authorization decorators
|
|
189
|
+
for decorator in node.decorator_list:
|
|
190
|
+
if isinstance(decorator, py_ast.Name):
|
|
191
|
+
if decorator.id.lower() in [
|
|
192
|
+
"login_required",
|
|
193
|
+
"auth_required",
|
|
194
|
+
"admin_required",
|
|
195
|
+
"permission_required",
|
|
196
|
+
]:
|
|
197
|
+
has_auth_decorator = True
|
|
198
|
+
break
|
|
199
|
+
elif isinstance(decorator, py_ast.Attribute):
|
|
200
|
+
if decorator.attr.lower() in [
|
|
201
|
+
"login_required",
|
|
202
|
+
"auth_required",
|
|
203
|
+
"admin_required",
|
|
204
|
+
"permission_required",
|
|
205
|
+
]:
|
|
206
|
+
has_auth_decorator = True
|
|
207
|
+
break
|
|
208
|
+
|
|
209
|
+
# Check if this looks like a route handler
|
|
210
|
+
has_route_decorator = any(
|
|
211
|
+
isinstance(d, py_ast.Attribute)
|
|
212
|
+
and d.attr
|
|
213
|
+
in (
|
|
214
|
+
"get",
|
|
215
|
+
"post",
|
|
216
|
+
"put",
|
|
217
|
+
"delete",
|
|
218
|
+
"patch",
|
|
219
|
+
"route",
|
|
220
|
+
"add_url_rule",
|
|
221
|
+
"before_request",
|
|
222
|
+
)
|
|
223
|
+
or (
|
|
224
|
+
isinstance(d, py_ast.Call)
|
|
225
|
+
and isinstance(d.func, py_ast.Attribute)
|
|
226
|
+
and d.func.attr
|
|
227
|
+
in (
|
|
228
|
+
"get",
|
|
229
|
+
"post",
|
|
230
|
+
"put",
|
|
231
|
+
"delete",
|
|
232
|
+
"patch",
|
|
233
|
+
)
|
|
234
|
+
)
|
|
235
|
+
for d in node.decorator_list
|
|
236
|
+
)
|
|
237
|
+
# Also flag functions named like route handlers (get_X, post_X, delete_X)
|
|
238
|
+
is_route_name = any(
|
|
239
|
+
func_name.lower().startswith(v)
|
|
240
|
+
for v in ("get_", "post_", "put_", "delete_", "patch_", "handle_")
|
|
241
|
+
)
|
|
242
|
+
if has_route_decorator or is_route_name:
|
|
243
|
+
if has_auth_decorator:
|
|
244
|
+
routes_with_auth.add(func_name)
|
|
245
|
+
else:
|
|
246
|
+
routes_without_auth.add(func_name)
|
|
247
|
+
|
|
248
|
+
# Report routes without authentication
|
|
249
|
+
for node in py_ast.walk(tree):
|
|
250
|
+
if isinstance(node, py_ast.FunctionDef) and node.name in routes_without_auth:
|
|
251
|
+
if node.name not in routes_with_auth:
|
|
252
|
+
findings.append(
|
|
253
|
+
make_finding(
|
|
254
|
+
severity=Severity.HIGH,
|
|
255
|
+
file=rel_path,
|
|
256
|
+
line_start=getattr(node, "lineno", 0),
|
|
257
|
+
title="Missing Authentication Check",
|
|
258
|
+
description=f"Route '{node.name}' may lack proper authentication/authorization",
|
|
259
|
+
evidence=f"Function {node.name} does not have authentication decorator",
|
|
260
|
+
)
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
# General pattern matching for Python
|
|
264
|
+
findings.extend(self._scan_general_authz(content, rel_path))
|
|
265
|
+
|
|
266
|
+
return findings
|
|
267
|
+
|
|
268
|
+
def _scan_javascript_authz(
|
|
269
|
+
self, content: str, rel_path: str, lang: Lang = Lang.JAVASCRIPT
|
|
270
|
+
) -> list[Finding]:
|
|
271
|
+
"""Scan JavaScript/TypeScript code for authorization vulnerabilities.
|
|
272
|
+
|
|
273
|
+
Uses tree-sitter to locate route-definition call expressions (e.g.
|
|
274
|
+
``app.get(...)`` / ``router.post(...)``) and inspects the full call
|
|
275
|
+
text (including any multi-line handler chain) for an authentication /
|
|
276
|
+
authorization guard. Falls back to the line-regex scanner if the file
|
|
277
|
+
cannot be parsed.
|
|
278
|
+
"""
|
|
279
|
+
tree = parse_source(content, lang)
|
|
280
|
+
if tree is None:
|
|
281
|
+
return self._scan_javascript_authz_regex(content, rel_path)
|
|
282
|
+
|
|
283
|
+
findings: list[Finding] = []
|
|
284
|
+
lines = content.splitlines()
|
|
285
|
+
route_verbs = {"get", "post", "put", "delete", "patch"}
|
|
286
|
+
auth_words = {
|
|
287
|
+
"authenticate",
|
|
288
|
+
"authorize",
|
|
289
|
+
"jwt",
|
|
290
|
+
"session",
|
|
291
|
+
"login",
|
|
292
|
+
"requireauth",
|
|
293
|
+
"isloggedin",
|
|
294
|
+
"permission",
|
|
295
|
+
"hasrole",
|
|
296
|
+
"hasauthority",
|
|
297
|
+
"secured",
|
|
298
|
+
"preauthorize",
|
|
299
|
+
}
|
|
300
|
+
sensitive_re = re.compile(
|
|
301
|
+
r"/api/.*(?:/users?|/admin|/settings|/profile|/account)", re.IGNORECASE
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
for node in self._iter_nodes(tree.root_node):
|
|
305
|
+
if node.type != "call_expression":
|
|
306
|
+
continue
|
|
307
|
+
fn = node.child_by_field_name("function")
|
|
308
|
+
if fn is None or fn.type != "member_expression":
|
|
309
|
+
continue
|
|
310
|
+
name = self._node_name(fn)
|
|
311
|
+
leaf = name.split(".")[-1].lower()
|
|
312
|
+
if leaf not in route_verbs:
|
|
313
|
+
continue
|
|
314
|
+
|
|
315
|
+
call_text = node_text(node)
|
|
316
|
+
line = node.start_point[0] + 1
|
|
317
|
+
lowered = call_text.lower()
|
|
318
|
+
has_auth = any(w in lowered for w in auth_words)
|
|
319
|
+
evidence_line = lines[line - 1].strip() if 0 < line <= len(lines) else call_text.strip()
|
|
320
|
+
if not has_auth:
|
|
321
|
+
sensitive = bool(sensitive_re.search(call_text))
|
|
322
|
+
findings.append(
|
|
323
|
+
make_finding(
|
|
324
|
+
severity=Severity.HIGH if sensitive else Severity.MEDIUM,
|
|
325
|
+
file=rel_path,
|
|
326
|
+
line_start=line,
|
|
327
|
+
title=(
|
|
328
|
+
"Sensitive Endpoint Without Authorization"
|
|
329
|
+
if sensitive
|
|
330
|
+
else "Potential Missing Authorization Check"
|
|
331
|
+
),
|
|
332
|
+
description="Route definition may lack authentication/authorization middleware",
|
|
333
|
+
evidence=evidence_line,
|
|
334
|
+
)
|
|
335
|
+
)
|
|
336
|
+
return findings
|
|
337
|
+
|
|
338
|
+
def _scan_javascript_authz_regex(self, content: str, rel_path: str) -> list[Finding]:
|
|
339
|
+
"""Line-based fallback for JS/TS when tree-sitter is unavailable."""
|
|
340
|
+
findings: list[Finding] = []
|
|
341
|
+
express_route_patterns = [
|
|
342
|
+
r'app\.(get|post|put|delete|patch)\s*\(\s*["\'][^"\']+["\']\s*,\s*(?!.*(?:authenticate|authorize|\bjwt\b|\bsession\b|\blogin\b|requireAuth|isLoggedIn)).*?=>',
|
|
343
|
+
r'router\.(get|post|put|delete|patch)\s*\(\s*["\'][^"\']+["\']\s*,\s*(?!.*(?:authenticate|authorize|\bjwt\b|\bsession\b|\blogin\b|requireAuth|isLoggedIn)).*?=>',
|
|
344
|
+
]
|
|
345
|
+
lines = content.splitlines()
|
|
346
|
+
for i, line in enumerate(lines, 1):
|
|
347
|
+
for pattern in express_route_patterns:
|
|
348
|
+
if re.search(pattern, line, re.IGNORECASE):
|
|
349
|
+
findings.append(
|
|
350
|
+
make_finding(
|
|
351
|
+
severity=Severity.MEDIUM,
|
|
352
|
+
file=rel_path,
|
|
353
|
+
line_start=i,
|
|
354
|
+
title="Potential Missing Authorization Check",
|
|
355
|
+
description="Route definition may lack authentication/authorization middleware",
|
|
356
|
+
evidence=line.strip(),
|
|
357
|
+
)
|
|
358
|
+
)
|
|
359
|
+
sensitive_re = re.compile(
|
|
360
|
+
r"/api/.*(?:/users?|/admin|/settings|/profile|/account)", re.IGNORECASE
|
|
361
|
+
)
|
|
362
|
+
for i, line in enumerate(lines, 1):
|
|
363
|
+
if sensitive_re.search(line) and not any(
|
|
364
|
+
w in line.lower() for w in ["auth", "login", "jwt", "session", "verify"]
|
|
365
|
+
):
|
|
366
|
+
findings.append(
|
|
367
|
+
make_finding(
|
|
368
|
+
severity=Severity.HIGH,
|
|
369
|
+
file=rel_path,
|
|
370
|
+
line_start=i,
|
|
371
|
+
title="Sensitive Endpoint Without Authorization",
|
|
372
|
+
description="API endpoint for sensitive data may lack authorization",
|
|
373
|
+
evidence=line.strip(),
|
|
374
|
+
)
|
|
375
|
+
)
|
|
376
|
+
return findings
|
|
377
|
+
|
|
378
|
+
# ── Shared tree-sitter helpers ─────────────────────────────────────────
|
|
379
|
+
|
|
380
|
+
def _iter_nodes(self, node):
|
|
381
|
+
yield node
|
|
382
|
+
for child in node.children:
|
|
383
|
+
yield from self._iter_nodes(child)
|
|
384
|
+
|
|
385
|
+
def _node_name(self, node: object) -> str:
|
|
386
|
+
"""Recursively reconstruct a dotted/scoped call name from a node."""
|
|
387
|
+
if node is None:
|
|
388
|
+
return ""
|
|
389
|
+
ntype = getattr(node, "type", "")
|
|
390
|
+
if ntype == "identifier":
|
|
391
|
+
return node_text(node)
|
|
392
|
+
if ntype == "member_expression":
|
|
393
|
+
obj = node.child_by_field_name("object")
|
|
394
|
+
prop = node.child_by_field_name("property")
|
|
395
|
+
if obj is not None and prop is not None:
|
|
396
|
+
return f"{self._node_name(obj)}.{node_text(prop)}"
|
|
397
|
+
return node_text(node)
|
|
398
|
+
if ntype in ("scoped_identifier", "qualified_name", "scoped_type_identifier"):
|
|
399
|
+
return node_text(node)
|
|
400
|
+
return node_text(node)
|
|
401
|
+
|
|
402
|
+
def _scan_java_authz(self, content: str, rel_path: str) -> list[Finding]:
|
|
403
|
+
"""Scan Java code for authorization vulnerabilities."""
|
|
404
|
+
findings = []
|
|
405
|
+
|
|
406
|
+
# Check if security annotations are present
|
|
407
|
+
has_security_annotation = any(
|
|
408
|
+
sec_word in content.lower()
|
|
409
|
+
for sec_word in [
|
|
410
|
+
"@secured",
|
|
411
|
+
"@preauthorize",
|
|
412
|
+
"@postauthorize",
|
|
413
|
+
"hasrole",
|
|
414
|
+
"hasauthority",
|
|
415
|
+
]
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
if not has_security_annotation:
|
|
419
|
+
lines = content.splitlines()
|
|
420
|
+
for i, line in enumerate(lines, 1):
|
|
421
|
+
# Look for controller or mapping annotations
|
|
422
|
+
if any(
|
|
423
|
+
annotation in line
|
|
424
|
+
for annotation in [
|
|
425
|
+
"@Controller",
|
|
426
|
+
"@RestController",
|
|
427
|
+
"@GetMapping",
|
|
428
|
+
"@PostMapping",
|
|
429
|
+
"@PutMapping",
|
|
430
|
+
"@DeleteMapping",
|
|
431
|
+
"@RequestMapping",
|
|
432
|
+
]
|
|
433
|
+
):
|
|
434
|
+
findings.append(
|
|
435
|
+
make_finding(
|
|
436
|
+
severity=Severity.MEDIUM,
|
|
437
|
+
file=rel_path,
|
|
438
|
+
line_start=i,
|
|
439
|
+
title="Potential Missing Authorization Check",
|
|
440
|
+
description="Controller or endpoint may lack authorization annotations",
|
|
441
|
+
evidence=line.strip(),
|
|
442
|
+
)
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
return findings
|
|
446
|
+
|
|
447
|
+
def _scan_php_authz(self, content: str, rel_path: str) -> list[Finding]:
|
|
448
|
+
"""Scan PHP code for authorization vulnerabilities."""
|
|
449
|
+
findings = []
|
|
450
|
+
|
|
451
|
+
# Look for PHP route definitions without auth checks
|
|
452
|
+
php_auth_patterns = [
|
|
453
|
+
r'Route::(get|post|put|delete|patch)\s*\(\s*["\'][^"\']+["\']\s*,\s*function\s*\(',
|
|
454
|
+
r"function\s+\w+Action\s*\(\s*\)",
|
|
455
|
+
]
|
|
456
|
+
|
|
457
|
+
lines = content.splitlines()
|
|
458
|
+
for i, line in enumerate(lines, 1):
|
|
459
|
+
for pattern in php_auth_patterns:
|
|
460
|
+
matches = re.finditer(pattern, line, re.IGNORECASE)
|
|
461
|
+
for _match in matches:
|
|
462
|
+
# Check if this line has authentication
|
|
463
|
+
if not any(
|
|
464
|
+
auth_word in content[max(0, i - 10) : i + 10]
|
|
465
|
+
for auth_word in [
|
|
466
|
+
"auth",
|
|
467
|
+
"Auth",
|
|
468
|
+
"login",
|
|
469
|
+
"Login",
|
|
470
|
+
"middleware",
|
|
471
|
+
"Middleware",
|
|
472
|
+
]
|
|
473
|
+
):
|
|
474
|
+
findings.append(
|
|
475
|
+
make_finding(
|
|
476
|
+
severity=Severity.MEDIUM,
|
|
477
|
+
file=rel_path,
|
|
478
|
+
line_start=i,
|
|
479
|
+
title="Potential Missing Authorization Check",
|
|
480
|
+
description="Route or controller method may lack authentication/authorization",
|
|
481
|
+
evidence=line.strip(),
|
|
482
|
+
)
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
return findings
|
|
486
|
+
|
|
487
|
+
def _scan_ruby_authz(self, content: str, rel_path: str) -> list[Finding]:
|
|
488
|
+
"""Scan Ruby code for authorization vulnerabilities."""
|
|
489
|
+
findings = []
|
|
490
|
+
|
|
491
|
+
# Look for Rails controller actions without authentication
|
|
492
|
+
ruby_auth_patterns = [
|
|
493
|
+
r"\bdef\s+(show|edit|update|destroy)\b",
|
|
494
|
+
]
|
|
495
|
+
|
|
496
|
+
# Check if authentication methods are called
|
|
497
|
+
has_auth_check = any(
|
|
498
|
+
auth_method in content
|
|
499
|
+
for auth_method in [
|
|
500
|
+
"before_action",
|
|
501
|
+
"before_filter",
|
|
502
|
+
"authenticate_",
|
|
503
|
+
"logged_in",
|
|
504
|
+
"authorized",
|
|
505
|
+
]
|
|
506
|
+
)
|
|
507
|
+
|
|
508
|
+
if not has_auth_check:
|
|
509
|
+
lines = content.splitlines()
|
|
510
|
+
for i, line in enumerate(lines, 1):
|
|
511
|
+
for pattern in ruby_auth_patterns:
|
|
512
|
+
matches = re.finditer(pattern, line)
|
|
513
|
+
for _match in matches:
|
|
514
|
+
findings.append(
|
|
515
|
+
make_finding(
|
|
516
|
+
severity=Severity.MEDIUM,
|
|
517
|
+
file=rel_path,
|
|
518
|
+
line_start=i,
|
|
519
|
+
title="Potential Missing Authorization Check",
|
|
520
|
+
description="Controller action may lack authentication/authorization",
|
|
521
|
+
evidence=line.strip(),
|
|
522
|
+
)
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
return findings
|
|
526
|
+
|
|
527
|
+
def _scan_treesitter_authz(self, content: str, rel_path: str, lang: Lang) -> list[Finding]:
|
|
528
|
+
"""Scan using tree-sitter for supported languages."""
|
|
529
|
+
findings = []
|
|
530
|
+
|
|
531
|
+
try:
|
|
532
|
+
parser, lang_obj = get_parser(lang)
|
|
533
|
+
parser.parse(bytes(content, "utf8"))
|
|
534
|
+
|
|
535
|
+
# For now, fall back to general pattern matching
|
|
536
|
+
findings.extend(self._scan_general_authz(content, rel_path))
|
|
537
|
+
except Exception as e:
|
|
538
|
+
# If tree-sitter fails, use general patterns
|
|
539
|
+
_log.warning("AuthZAgent._scan_treesitter_authz failed: %s", e)
|
|
540
|
+
findings.extend(self._scan_general_authz(content, rel_path))
|
|
541
|
+
|
|
542
|
+
return findings
|
|
543
|
+
|
|
544
|
+
def _scan_general_authz(self, content: str, rel_path: str) -> list[Finding]:
|
|
545
|
+
"""Apply general authorization pattern matching."""
|
|
546
|
+
findings = []
|
|
547
|
+
|
|
548
|
+
# General authorization patterns
|
|
549
|
+
authz_patterns = [
|
|
550
|
+
(
|
|
551
|
+
r"\b(?:delete|remove|update|modify)\b.*\buser\b",
|
|
552
|
+
"Potential Missing Authorization",
|
|
553
|
+
Severity.HIGH,
|
|
554
|
+
),
|
|
555
|
+
(
|
|
556
|
+
r"\b(?:change|set|update)\b.*\bpassword\b",
|
|
557
|
+
"Potential Missing Authorization",
|
|
558
|
+
Severity.HIGH,
|
|
559
|
+
),
|
|
560
|
+
(
|
|
561
|
+
r"\b(?:admin|administrator|superuser)\b",
|
|
562
|
+
"Admin Functionality",
|
|
563
|
+
Severity.MEDIUM,
|
|
564
|
+
),
|
|
565
|
+
(
|
|
566
|
+
r"\b(?:get|fetch|retrieve)\b.*\b(?:config|setting)\b",
|
|
567
|
+
"Configuration Access",
|
|
568
|
+
Severity.MEDIUM,
|
|
569
|
+
),
|
|
570
|
+
]
|
|
571
|
+
|
|
572
|
+
lines = content.splitlines()
|
|
573
|
+
for i, line in enumerate(lines, 1):
|
|
574
|
+
for pattern, description, severity in authz_patterns:
|
|
575
|
+
matches = re.finditer(pattern, line, re.IGNORECASE)
|
|
576
|
+
for _match in matches:
|
|
577
|
+
# Only flag if there's no obvious auth check in the vicinity
|
|
578
|
+
context = " ".join(lines[max(0, i - 3) : min(len(lines), i + 3)])
|
|
579
|
+
if not any(
|
|
580
|
+
auth_word in context.lower()
|
|
581
|
+
for auth_word in [
|
|
582
|
+
"auth",
|
|
583
|
+
"login",
|
|
584
|
+
"verify",
|
|
585
|
+
"validate",
|
|
586
|
+
"permission",
|
|
587
|
+
"authorize",
|
|
588
|
+
"allow",
|
|
589
|
+
]
|
|
590
|
+
):
|
|
591
|
+
findings.append(
|
|
592
|
+
make_finding(
|
|
593
|
+
severity=severity,
|
|
594
|
+
file=rel_path,
|
|
595
|
+
line_start=i,
|
|
596
|
+
title=description,
|
|
597
|
+
description=f"Found potential {description.lower()} without clear authorization check",
|
|
598
|
+
evidence=line.strip(),
|
|
599
|
+
)
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
return findings
|