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,417 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PushNotificationAgent — push notification security.
|
|
3
|
+
|
|
4
|
+
Detects push notification security issues:
|
|
5
|
+
- Hardcoded FCM server keys or APNs private keys
|
|
6
|
+
- Push token handling vulnerabilities (storage, transmission)
|
|
7
|
+
- Sensitive data in notification payloads
|
|
8
|
+
- Missing token rotation/invalidation
|
|
9
|
+
- Insecure push notification endpoints
|
|
10
|
+
- Missing push notification authentication
|
|
11
|
+
|
|
12
|
+
Uses static file analysis and optional external tools (gitleaks).
|
|
13
|
+
Does NOT call AI.
|
|
14
|
+
Does NOT write to disk.
|
|
15
|
+
Does NOT touch the queue.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import logging
|
|
21
|
+
import re
|
|
22
|
+
import shutil
|
|
23
|
+
import subprocess
|
|
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.trace_log import trace_agent
|
|
40
|
+
|
|
41
|
+
_SOURCE_EXTENSIONS = {
|
|
42
|
+
"*.py",
|
|
43
|
+
"*.js",
|
|
44
|
+
"*.jsx",
|
|
45
|
+
"*.ts",
|
|
46
|
+
"*.tsx",
|
|
47
|
+
"*.java",
|
|
48
|
+
"*.php",
|
|
49
|
+
"*.rb",
|
|
50
|
+
"*.go",
|
|
51
|
+
"*.rs",
|
|
52
|
+
"*.cs",
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
_FCM_KEY_PATTERNS = [
|
|
56
|
+
(
|
|
57
|
+
re.compile(
|
|
58
|
+
r"(?:FCM[_-]?(?:SERVER[_-]?KEY|KEY|SECRET|TOKEN|API[_-]?KEY))\s*[:=]\s*['\"][A-Za-z0-9_\-]{20,}['\"]",
|
|
59
|
+
re.IGNORECASE,
|
|
60
|
+
),
|
|
61
|
+
"PUSH-01: Hardcoded FCM Server Key",
|
|
62
|
+
),
|
|
63
|
+
(
|
|
64
|
+
re.compile(
|
|
65
|
+
r"(?:server[_-]?key|fcm[_-]?key)\s*[:=]\s*['\"][A-Za-z0-9_\-]{20,}['\"]", re.IGNORECASE
|
|
66
|
+
),
|
|
67
|
+
"PUSH-01: Hardcoded FCM Key",
|
|
68
|
+
),
|
|
69
|
+
(
|
|
70
|
+
re.compile(r"AAAA[A-Za-z0-9_-]{7}:[A-Za-z0-9_-]{140}", re.IGNORECASE),
|
|
71
|
+
"PUSH-01: Exposed FCM Server Key",
|
|
72
|
+
),
|
|
73
|
+
]
|
|
74
|
+
|
|
75
|
+
_APNS_KEY_PATTERNS = [
|
|
76
|
+
(
|
|
77
|
+
re.compile(
|
|
78
|
+
r"(?:APNS[_-]?(?:KEY|TOKEN|SECRET|PRIVATE[_-]?KEY|CERT))\s*[:=]\s*['\"][^'\"]{10,}['\"]",
|
|
79
|
+
re.IGNORECASE,
|
|
80
|
+
),
|
|
81
|
+
"PUSH-02: Hardcoded APNs Credential",
|
|
82
|
+
),
|
|
83
|
+
(
|
|
84
|
+
re.compile(
|
|
85
|
+
r"(?:aps[_-]?env|apns[_-]?environment)\s*[:=]\s*['\"]?(?:production|development|sandbox)['\"]?",
|
|
86
|
+
re.IGNORECASE,
|
|
87
|
+
),
|
|
88
|
+
"PUSH-03: APNs Environment Hardcoded",
|
|
89
|
+
),
|
|
90
|
+
(
|
|
91
|
+
re.compile(r"-----BEGIN (?:RSA )?PRIVATE KEY-----"),
|
|
92
|
+
"PUSH-02: Embedded Private Key in Source",
|
|
93
|
+
),
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
_PUSH_TOKEN_PATTERNS = [
|
|
97
|
+
(
|
|
98
|
+
re.compile(
|
|
99
|
+
r"(?:device[_-]?token|push[_-]?token|fcm[_-]?token|apns[_-]?token)\s*[:=]\s*['\"][A-Za-z0-9_\-]{20,}['\"]",
|
|
100
|
+
re.IGNORECASE,
|
|
101
|
+
),
|
|
102
|
+
"PUSH-04: Hardcoded Push Token",
|
|
103
|
+
),
|
|
104
|
+
(
|
|
105
|
+
re.compile(
|
|
106
|
+
r"(?:token|device_token)\s*\.\s*(?:save|store|persist|write|insert)", re.IGNORECASE
|
|
107
|
+
),
|
|
108
|
+
"PUSH-05: Push Token Stored Without Encryption",
|
|
109
|
+
),
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
_SENSITIVE_PAYLOAD_PATTERNS = [
|
|
113
|
+
(
|
|
114
|
+
re.compile(
|
|
115
|
+
r"(?:password|secret|credit.?card|ssn|social.?security|api.?key|private.?key)\s*[:=]",
|
|
116
|
+
re.IGNORECASE,
|
|
117
|
+
),
|
|
118
|
+
"PUSH-06: Sensitive Data in Notification Payload",
|
|
119
|
+
),
|
|
120
|
+
(
|
|
121
|
+
re.compile(
|
|
122
|
+
r"(?:notification|message|alert|payload|body)\s*(?:\.|\[).*(?:password|secret|token|key|credential)",
|
|
123
|
+
re.IGNORECASE,
|
|
124
|
+
),
|
|
125
|
+
"PUSH-06: Sensitive Data in Notification Body",
|
|
126
|
+
),
|
|
127
|
+
]
|
|
128
|
+
|
|
129
|
+
_NOTIFICATION_FRAMEWORKS = [
|
|
130
|
+
re.compile(r"\bfirebase[_-]?admin\b|\bfcm\b|FirebaseMessaging", re.IGNORECASE),
|
|
131
|
+
re.compile(r"\bapns[_-]?client\b|\bpyapns\b|APNs", re.IGNORECASE),
|
|
132
|
+
re.compile(r"\bpush[_-]?notification\b|\bpush[_-]?service\b", re.IGNORECASE),
|
|
133
|
+
re.compile(r"\bOneSignal\b|\bonesignal\b", re.IGNORECASE),
|
|
134
|
+
re.compile(r"\bexpo[_-]?server[_-]?sdk\b|\bexpo[_-]?notifications\b", re.IGNORECASE),
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
_log = logging.getLogger("patchi.security.push_notification_agent")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@register
|
|
142
|
+
class PushNotificationAgent(BaseAgent):
|
|
143
|
+
"""Agent for detecting push notification security issues."""
|
|
144
|
+
|
|
145
|
+
group = AgentGroup.SECURITY
|
|
146
|
+
domain = AgentDomain.SECURITY
|
|
147
|
+
name = "PushNotificationAgent"
|
|
148
|
+
description = "Push notification security: FCM/APNs credential storage, token handling, payload minimization"
|
|
149
|
+
|
|
150
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
151
|
+
findings: list[Finding] = []
|
|
152
|
+
files_scanned = 0
|
|
153
|
+
|
|
154
|
+
with trace_agent(self.name, inp.root) as trace:
|
|
155
|
+
# ── Static analysis ────────────────────────────────────────────
|
|
156
|
+
for pattern in _SOURCE_EXTENSIONS:
|
|
157
|
+
for fp in safe_rglob(inp.root, pattern):
|
|
158
|
+
if fp.is_file():
|
|
159
|
+
files_scanned += 1
|
|
160
|
+
findings.extend(self._scan_file(fp, inp.root))
|
|
161
|
+
|
|
162
|
+
findings.extend(self._scan_push_config(inp))
|
|
163
|
+
findings.extend(self._scan_token_storage(inp))
|
|
164
|
+
|
|
165
|
+
# ── External tool: gitleaks ────────────────────────────────────
|
|
166
|
+
if shutil.which("gitleaks"):
|
|
167
|
+
findings.extend(self._run_gitleaks(inp))
|
|
168
|
+
|
|
169
|
+
trace.findings = len(findings)
|
|
170
|
+
trace.files_scanned = files_scanned
|
|
171
|
+
|
|
172
|
+
result.status = AgentStatus.SUCCEEDED
|
|
173
|
+
result.findings = findings
|
|
174
|
+
result.data.update(
|
|
175
|
+
{
|
|
176
|
+
"push_findings": len(findings),
|
|
177
|
+
"gitleaks_available": shutil.which("gitleaks") is not None,
|
|
178
|
+
}
|
|
179
|
+
)
|
|
180
|
+
return
|
|
181
|
+
|
|
182
|
+
# ── Source file scan ───────────────────────────────────────────────────
|
|
183
|
+
|
|
184
|
+
def _scan_file(self, fp: Path, root: Path) -> list[Finding]:
|
|
185
|
+
findings: list[Finding] = []
|
|
186
|
+
rel = fp.relative_to(root).as_posix()
|
|
187
|
+
try:
|
|
188
|
+
content = fp.read_text(encoding="utf-8", errors="replace")
|
|
189
|
+
lines = content.splitlines()
|
|
190
|
+
|
|
191
|
+
# FCM keys
|
|
192
|
+
for i, line in enumerate(lines, 1):
|
|
193
|
+
for rx, title in _FCM_KEY_PATTERNS:
|
|
194
|
+
if rx.search(line):
|
|
195
|
+
findings.append(
|
|
196
|
+
make_finding(
|
|
197
|
+
severity=Severity.CRITICAL,
|
|
198
|
+
file=rel,
|
|
199
|
+
line_start=i,
|
|
200
|
+
title=title,
|
|
201
|
+
description="Hardcoded FCM server key found. Anyone with this key can send push notifications.",
|
|
202
|
+
evidence=line.strip()[:120],
|
|
203
|
+
suggestion="Store FCM server keys in environment variables or a secrets manager.",
|
|
204
|
+
)
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# APNs keys
|
|
208
|
+
for i, line in enumerate(lines, 1):
|
|
209
|
+
for rx, title in _APNS_KEY_PATTERNS:
|
|
210
|
+
if rx.search(line):
|
|
211
|
+
findings.append(
|
|
212
|
+
make_finding(
|
|
213
|
+
severity=Severity.CRITICAL,
|
|
214
|
+
file=rel,
|
|
215
|
+
line_start=i,
|
|
216
|
+
title=title,
|
|
217
|
+
description="Hardcoded APNs credential or private key found.",
|
|
218
|
+
evidence=line.strip()[:120],
|
|
219
|
+
suggestion="Store APNs credentials securely and load from environment variables.",
|
|
220
|
+
)
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# Push tokens
|
|
224
|
+
for i, line in enumerate(lines, 1):
|
|
225
|
+
for rx, title in _PUSH_TOKEN_PATTERNS:
|
|
226
|
+
if rx.search(line):
|
|
227
|
+
findings.append(
|
|
228
|
+
make_finding(
|
|
229
|
+
severity=Severity.HIGH,
|
|
230
|
+
file=rel,
|
|
231
|
+
line_start=i,
|
|
232
|
+
title=title,
|
|
233
|
+
description="Push token handling issue detected.",
|
|
234
|
+
evidence=line.strip()[:120],
|
|
235
|
+
suggestion="Rotate push tokens regularly and encrypt at rest.",
|
|
236
|
+
)
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
# Sensitive data in payloads
|
|
240
|
+
in_notification_block = False
|
|
241
|
+
for i, line in enumerate(lines, 1):
|
|
242
|
+
if re.search(
|
|
243
|
+
r"(?:notification|message|payload|alert|body)\s*[{(]", line, re.IGNORECASE
|
|
244
|
+
):
|
|
245
|
+
in_notification_block = True
|
|
246
|
+
if in_notification_block:
|
|
247
|
+
for rx, title in _SENSITIVE_PAYLOAD_PATTERNS:
|
|
248
|
+
if rx.search(line):
|
|
249
|
+
findings.append(
|
|
250
|
+
make_finding(
|
|
251
|
+
severity=Severity.HIGH,
|
|
252
|
+
file=rel,
|
|
253
|
+
line_start=i,
|
|
254
|
+
title=title,
|
|
255
|
+
description="Sensitive data detected in notification payload.",
|
|
256
|
+
evidence=line.strip()[:120],
|
|
257
|
+
suggestion="Minimize notification payloads; never include PII or secrets.",
|
|
258
|
+
)
|
|
259
|
+
)
|
|
260
|
+
if re.search(r"[})]", line):
|
|
261
|
+
in_notification_block = False
|
|
262
|
+
|
|
263
|
+
except Exception as e:
|
|
264
|
+
_log.warning("PushNotificationAgent._scan_file failed: %s", e)
|
|
265
|
+
return findings
|
|
266
|
+
|
|
267
|
+
# ── Push config scan ───────────────────────────────────────────────────
|
|
268
|
+
|
|
269
|
+
def _scan_push_config(self, inp: AgentInput) -> list[Finding]:
|
|
270
|
+
findings: list[Finding] = []
|
|
271
|
+
config_patterns = ("*.yaml", "*.yml", "*.json", "*.toml", "*.tf", "*.env*")
|
|
272
|
+
for pattern in config_patterns:
|
|
273
|
+
for fp in safe_rglob(inp.root, pattern):
|
|
274
|
+
if not fp.is_file():
|
|
275
|
+
continue
|
|
276
|
+
rel = fp.relative_to(inp.root).as_posix()
|
|
277
|
+
try:
|
|
278
|
+
content = fp.read_text(encoding="utf-8", errors="replace")
|
|
279
|
+
content_lower = content.lower()
|
|
280
|
+
# Check if file is push-related
|
|
281
|
+
is_push_config = any(
|
|
282
|
+
kw in content_lower
|
|
283
|
+
for kw in ("fcm", "firebase", "apns", "push", "notification")
|
|
284
|
+
)
|
|
285
|
+
if not is_push_config:
|
|
286
|
+
continue
|
|
287
|
+
|
|
288
|
+
# Check for credentials in config
|
|
289
|
+
for rx, title in _FCM_KEY_PATTERNS:
|
|
290
|
+
if rx.search(content):
|
|
291
|
+
findings.append(
|
|
292
|
+
make_finding(
|
|
293
|
+
severity=Severity.CRITICAL,
|
|
294
|
+
file=rel,
|
|
295
|
+
line_start=0,
|
|
296
|
+
title=title,
|
|
297
|
+
description="FCM server key found in configuration file.",
|
|
298
|
+
suggestion="Use environment variables or a secrets manager instead of config files.",
|
|
299
|
+
)
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
for rx, title in _APNS_KEY_PATTERNS:
|
|
303
|
+
if rx.search(content):
|
|
304
|
+
findings.append(
|
|
305
|
+
make_finding(
|
|
306
|
+
severity=Severity.CRITICAL,
|
|
307
|
+
file=rel,
|
|
308
|
+
line_start=0,
|
|
309
|
+
title=title,
|
|
310
|
+
description="APNs credential found in configuration file.",
|
|
311
|
+
suggestion="Load APNs credentials from a secure vault.",
|
|
312
|
+
)
|
|
313
|
+
)
|
|
314
|
+
|
|
315
|
+
except Exception as e:
|
|
316
|
+
_log.warning("PushNotificationAgent._scan_push_config failed: %s", e)
|
|
317
|
+
return findings
|
|
318
|
+
|
|
319
|
+
# ── Token storage patterns ─────────────────────────────────────────────
|
|
320
|
+
|
|
321
|
+
def _scan_token_storage(self, inp: AgentInput) -> list[Finding]:
|
|
322
|
+
findings: list[Finding] = []
|
|
323
|
+
for pattern in ("*.py", "*.js", "*.ts", "*.java", "*.go", "*.rb"):
|
|
324
|
+
for fp in safe_rglob(inp.root, pattern):
|
|
325
|
+
if not fp.is_file():
|
|
326
|
+
continue
|
|
327
|
+
rel = fp.relative_to(inp.root).as_posix()
|
|
328
|
+
try:
|
|
329
|
+
content = fp.read_text(encoding="utf-8", errors="replace")
|
|
330
|
+
lines = content.splitlines()
|
|
331
|
+
for i, line in enumerate(lines, 1):
|
|
332
|
+
# Token stored in plaintext database field
|
|
333
|
+
if re.search(
|
|
334
|
+
r"(?:device_token|push_token|fcm_token|apns_token)\s*.*(?:VARCHAR|TEXT|STRING|str)",
|
|
335
|
+
line,
|
|
336
|
+
re.IGNORECASE,
|
|
337
|
+
):
|
|
338
|
+
findings.append(
|
|
339
|
+
make_finding(
|
|
340
|
+
severity=Severity.MEDIUM,
|
|
341
|
+
file=rel,
|
|
342
|
+
line_start=i,
|
|
343
|
+
title="PUSH-05: Push Token Stored Without Encryption",
|
|
344
|
+
description="Push token stored in a plaintext database column.",
|
|
345
|
+
evidence=line.strip()[:120],
|
|
346
|
+
suggestion="Encrypt push tokens at rest or store hashed.",
|
|
347
|
+
)
|
|
348
|
+
)
|
|
349
|
+
# Token logged
|
|
350
|
+
if re.search(
|
|
351
|
+
r"(?:log|print|debug|console).*(?:token|device_token|push_token)",
|
|
352
|
+
line,
|
|
353
|
+
re.IGNORECASE,
|
|
354
|
+
):
|
|
355
|
+
findings.append(
|
|
356
|
+
make_finding(
|
|
357
|
+
severity=Severity.MEDIUM,
|
|
358
|
+
file=rel,
|
|
359
|
+
line_start=i,
|
|
360
|
+
title="PUSH-04: Push Token Logged",
|
|
361
|
+
description="Push token may be written to logs.",
|
|
362
|
+
evidence=line.strip()[:120],
|
|
363
|
+
suggestion="Never log push tokens; they can be used to impersonate users.",
|
|
364
|
+
)
|
|
365
|
+
)
|
|
366
|
+
except Exception as e:
|
|
367
|
+
_log.warning("PushNotificationAgent._scan_token_storage failed: %s", e)
|
|
368
|
+
return findings
|
|
369
|
+
|
|
370
|
+
# ── External tool: gitleaks ────────────────────────────────────────────
|
|
371
|
+
|
|
372
|
+
def _run_gitleaks(self, inp: AgentInput) -> list[Finding]:
|
|
373
|
+
"""Run gitleaks to detect hardcoded push notification secrets."""
|
|
374
|
+
findings: list[Finding] = []
|
|
375
|
+
try:
|
|
376
|
+
proc = subprocess.run(
|
|
377
|
+
[
|
|
378
|
+
"gitleaks",
|
|
379
|
+
"detect",
|
|
380
|
+
"--source",
|
|
381
|
+
str(inp.root),
|
|
382
|
+
"--report-format",
|
|
383
|
+
"json",
|
|
384
|
+
"--redact",
|
|
385
|
+
],
|
|
386
|
+
capture_output=True,
|
|
387
|
+
text=True,
|
|
388
|
+
timeout=120,
|
|
389
|
+
)
|
|
390
|
+
if proc.stdout.strip():
|
|
391
|
+
import json
|
|
392
|
+
|
|
393
|
+
try:
|
|
394
|
+
data = json.loads(proc.stdout)
|
|
395
|
+
for item in data if isinstance(data, list) else []:
|
|
396
|
+
rule = item.get("RuleID", "").lower()
|
|
397
|
+
description = item.get("Description", "")
|
|
398
|
+
if any(
|
|
399
|
+
kw in rule or kw in description.lower()
|
|
400
|
+
for kw in ("fcm", "firebase", "apns", "push", "notification")
|
|
401
|
+
):
|
|
402
|
+
findings.append(
|
|
403
|
+
make_finding(
|
|
404
|
+
severity=Severity.CRITICAL,
|
|
405
|
+
file=item.get("File", "(gitleaks)"),
|
|
406
|
+
line_start=item.get("StartLine", 0),
|
|
407
|
+
title="PUSH-01: Push Secret Detected by Gitleaks",
|
|
408
|
+
description=f"Gitleaks detected: {item.get('RuleID', 'unknown')}",
|
|
409
|
+
evidence=item.get("Match", "")[:120],
|
|
410
|
+
suggestion="Remove the secret and rotate credentials immediately.",
|
|
411
|
+
)
|
|
412
|
+
)
|
|
413
|
+
except json.JSONDecodeError:
|
|
414
|
+
pass
|
|
415
|
+
except (subprocess.TimeoutExpired, FileNotFoundError, OSError):
|
|
416
|
+
pass
|
|
417
|
+
return findings
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""
|
|
2
|
+
PysaAgent - Python Static Analyzer integration.
|
|
3
|
+
|
|
4
|
+
Pysa (Python Static Analyzer) is a static analysis tool for Python that focuses on
|
|
5
|
+
security vulnerabilities like SQL injection, XSS, and other taint-style vulnerabilities.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import logging
|
|
11
|
+
import subprocess
|
|
12
|
+
import tempfile
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from patchi.core.agents.base import (
|
|
17
|
+
AgentDomain,
|
|
18
|
+
AgentGroup,
|
|
19
|
+
AgentInput,
|
|
20
|
+
AgentResult,
|
|
21
|
+
AgentStatus,
|
|
22
|
+
BaseAgent,
|
|
23
|
+
register,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
_log = logging.getLogger("patchi.security.agents.pysa_agent")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@register
|
|
30
|
+
class PysaAgent(BaseAgent):
|
|
31
|
+
"""
|
|
32
|
+
PysaAgent - Python Static Analyzer integration.
|
|
33
|
+
|
|
34
|
+
Pysa is a static analysis tool for Python that focuses on security vulnerabilities
|
|
35
|
+
through taint analysis. It tracks data flow from sources to sinks.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
group = AgentGroup.SECURITY
|
|
39
|
+
domain = AgentDomain.CODE_QUALITY
|
|
40
|
+
name = "PysaAgent"
|
|
41
|
+
description = "Pysa static analyzer for Python security vulnerabilities"
|
|
42
|
+
timeout = 120
|
|
43
|
+
|
|
44
|
+
def _run(self, inp: AgentInput, result: AgentResult) -> None:
|
|
45
|
+
"""Run Pysa analysis on the project."""
|
|
46
|
+
if not self._is_pysa_available():
|
|
47
|
+
result.status = AgentStatus.SKIPPED
|
|
48
|
+
result.data["error"] = "Pysa not installed"
|
|
49
|
+
return
|
|
50
|
+
|
|
51
|
+
findings = self._run_pysa(inp.root)
|
|
52
|
+
|
|
53
|
+
for finding_data in findings:
|
|
54
|
+
finding = self._create_finding(finding_data)
|
|
55
|
+
result.add_finding(finding)
|
|
56
|
+
|
|
57
|
+
result.status = AgentStatus.DONE
|
|
58
|
+
result.files_scanned = 1
|
|
59
|
+
|
|
60
|
+
def _is_pysa_available(self) -> bool:
|
|
61
|
+
"""Check if Pysa is installed and available."""
|
|
62
|
+
try:
|
|
63
|
+
result = subprocess.run(["pysa", "--version"], capture_output=True, timeout=10)
|
|
64
|
+
return result.returncode == 0
|
|
65
|
+
except FileNotFoundError:
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
def _run_pysa(self, root: Path) -> list[dict[str, Any]]:
|
|
69
|
+
"""Run Pysa and parse JSON output."""
|
|
70
|
+
findings = []
|
|
71
|
+
|
|
72
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
73
|
+
Path(tmpdir) / "pysa_output.json"
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
subprocess.run(
|
|
77
|
+
[
|
|
78
|
+
"pysa",
|
|
79
|
+
"analyze",
|
|
80
|
+
"--output-format",
|
|
81
|
+
"json",
|
|
82
|
+
"--output",
|
|
83
|
+
str(tmpdir / "pysa_output"),
|
|
84
|
+
str(root),
|
|
85
|
+
],
|
|
86
|
+
capture_output=True,
|
|
87
|
+
timeout=120,
|
|
88
|
+
cwd=root,
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Parse Pysa JSON output
|
|
92
|
+
output_dir = Path(tmpdir) / "pysa_output"
|
|
93
|
+
if output_dir.exists():
|
|
94
|
+
for json_file in output_dir.glob("*.json"):
|
|
95
|
+
with open(json_file) as f:
|
|
96
|
+
data = json.load(f)
|
|
97
|
+
findings.extend(self._parse_pysa_output(data))
|
|
98
|
+
|
|
99
|
+
except subprocess.TimeoutExpired:
|
|
100
|
+
_log.warning("Pysa timed out")
|
|
101
|
+
except Exception as e:
|
|
102
|
+
_log.warning(f"Pysa execution failed: {e}")
|
|
103
|
+
|
|
104
|
+
return findings
|
|
105
|
+
|
|
106
|
+
def _parse_pysa_output(self, data: dict[str, Any]) -> list[dict[str, Any]]:
|
|
107
|
+
"""Parse Pysa JSON output into findings."""
|
|
108
|
+
findings = []
|
|
109
|
+
|
|
110
|
+
for issue in data.get("issues", []):
|
|
111
|
+
findings.append(
|
|
112
|
+
{
|
|
113
|
+
"type": issue.get("code", "pysa_finding"),
|
|
114
|
+
"severity": issue.get("severity", "MEDIUM").upper(),
|
|
115
|
+
"file": issue.get("file", ""),
|
|
116
|
+
"line": issue.get("line", 0),
|
|
117
|
+
"message": issue.get("message", ""),
|
|
118
|
+
"cwe": issue.get("cwe", ""),
|
|
119
|
+
"confidence": 0.8,
|
|
120
|
+
}
|
|
121
|
+
)
|
|
122
|
+
return findings
|
|
123
|
+
|
|
124
|
+
def _create_finding(self, finding_data: dict[str, Any]):
|
|
125
|
+
"""Create a Finding object from Pysa output."""
|
|
126
|
+
from patchi.core.security.tool_adapters import make_tool_finding
|
|
127
|
+
|
|
128
|
+
return make_tool_finding(
|
|
129
|
+
agent="PysaAgent",
|
|
130
|
+
ftype=finding_data.get("type", "pysa_finding"),
|
|
131
|
+
raw_severity=finding_data.get("severity", "MEDIUM"),
|
|
132
|
+
file=finding_data.get("file", ""),
|
|
133
|
+
line=finding_data.get("line", 0),
|
|
134
|
+
message=finding_data.get("message", ""),
|
|
135
|
+
cwe=finding_data.get("cwe", ""),
|
|
136
|
+
snippet=finding_data.get("snippet", ""),
|
|
137
|
+
confidence_raw=0.8, # taint-path findings: tool asserts a full flow
|
|
138
|
+
extra={"flow": finding_data.get("flow", [])},
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# Register the agent
|
|
143
|
+
register(PysaAgent)
|